0%

【译文】Python中关于星号你所需要知道的一切

英文原文:All you need to know about Asterisks in Python
原文作者:bascodes

大多数开发人员都知道星号(*)字符是 Python 中的乘法运算符:

1
product = 4 * 2 # 8

但是,对于列表或字典数据结构而言,星号有一些特殊含义。

*args**kwargs

函数定义中的 *args

星号操作符(也称为解构操作符)在 Python 中最广为人知的用法是在函数中使用。

假设有一个函数,它可以累加值并返回这些值的和:

1
2
3
4
def add(number_1, number_2):
return number_1 + number_2

print(add(1,2)) # 3

如果你想对任意数量的值求和呢?可以在参数名称前添加星号,如这样:

1
2
3
4
5
def add(*numbers):
sum = 0
for number in numbers:
sum += number
return sum

正如你可能已从函数体中注意到,我们期望 numbers 是一个可迭代对象。实际上,如果你检查其类型,numbers 是一个 tuple。而且,实际上,我们现在可以用任意数量的参数调用该函数:

1
add(1, 2, 3, 4) # 10

函数调用中的 *

到目前为止,我们已经知道可以定义一个函数,使它接受一系列参数。但是如果我们有一个函数,它有一组固定的参数,我们想要传递一系列值给它。考虑这个函数:

1
2
def add(number_1, number_2, number_3):
return number_1 + number_2 + number_3

这个函数正好接受 3 个参数。假设我们有一个正好有三个元素的列表。当然,我们可以这样调用函数:

1
2
3
my_list = [1, 2, 3]

add(my_list[0], my_list[1], my_list[2])

幸运的是,解构操作符(*)可以以两种方式工作。我们已经在函数的定义中看到了它的用法,但是我们也可以使用它来调用函数:

1
2
3
my_list = [1, 2, 3]

add(*my_list)

函数定义中的 **kwargs

同样地,我们可以使用星号(*)操作符来解构列表,我们也可以使用双星号(**)操作符来解构 Python 函数中的字典。

考虑一个这样的函数:

1
2
3
4
5
def change_user_details(username, email, phone, date_of_birth, street_address):
user = get_user(username)
user.email = email
user.phone = phone
...

如果使用关键字参数(kwargs)调用它,函数调用可能如下所示:

1
change_user_details('bascodes', email='blog@bascodes.example.com', phone='...', ...)

在这种情况下,我们可以像这样重写函数,以接受任意数量的关键字参数,然后将这些参数表示为一个名为 kwargs 的字典:

1
2
3
4
5
def change_user_details(username, **kwargs):
user = get_user(username)
user.email = kwargs['email']
user.phone = kwargs['phone']
...

当然,我们可以像使用 Python 中的任何其他字典一样使用 kwargs 字典,因此,如果我们像这样实际使用字典数据结构,那么函数可能会变得更清晰一些:

1
2
3
4
5
def change_user_details(username, **kwargs):
user = get_user(username)
for attribute, value in kwargs.items():
setattr(user, attribute, value)
...

函数调用中的 **

当然,** 操作符也适用于调用函数:

1
2
3
4
5
details = {
'email': 'blog@bascodes.example.com',
...
}
change_user_detail('bascodes', **details)

限制如何调用函数

仅关键字参数

函数定义中星号最令人惊讶的特性之一是它可以单独使用,即不需要变量(参数)名。也就是说,这是 Python 中一个完全有效的函数定义:

1
2
def my_function(*, keyword_arg_1):
...

但是,在这种情况下,独立的星号有什么作用呢?如上所述,星号在列表中捕获所有(非关键字)参数。在我们的示例中,没有变量名可出现在参数列表中。在 * 之后我们有一个名为 keyword_arg_1 的变量。因为 * 已经匹配了所有的位置参数,我们剩下的 keyword_arg_1,它必须用作关键字参数。

使用 my_function(1) 调用上面的函数会抛出一个错误:

1
TypeError: my_function() takes 0 positional arguments but 1 was given

仅位置参数

与上个示例中仅使用关键字参数相反,如果我们想强制函数的用户仅使用位置参数,该怎么办呢?

好吧,有一种非常 Pythonic 的方法。我们说明下使用 / 符号(与乘法相反)的处理技巧:

1
2
def only_positional_arguments(arg1, arg2, /):
...

令人惊讶的是,很少有 Python 开发人员知道这个通过 PEP 570 引入到 Python 3.8 的技巧。

如果你使用 only_positional_arguments(arg1=1, arg2=2) 调用上面的函数,将会抛出一个 TypeError:

1
TypeError: only_positional_arguments() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

在字面量中使用 ***

星号(*)和双星号(**)操作符不仅适用于函数定义和调用,还可用于构造列表笔字典。

构造列表

假设,我们有两个列表,并想合并它们。

1
2
my_list_1 = [1, 2, 3]
my_list_2 = [10, 20, 30]

当然,我们可以用 + 操作符合并这些列表:

1
merged_list = my_list_1 + my_list_2

然而,* 操作符给了我们更多的灵活性。假设,我们想在中间插入一个标量值,我们可以使用:

1
2
3
4
some_value = 42
merged_list = [*my_list_1, some_value, *my_list_2]

# [1, 2, 3, 42, 10, 20, 30]

构造字典

适用于列表和星号(*)操作符的,同样适用于字典和双星号(**)。

1
2
3
4
5
6
7
8
9
10
social_media_details = {
'twitter': 'bascodes'
}

contact_details = {
'email': 'blog@bascodes.example.com'
}

user_dict = {'username': 'bas', **social_media_details, **contact_details}
# {'email': 'blog@bascodes.example.com', 'twitter': 'bascodes', 'username': 'bas'}

解构列表

你可能已经知道,可以像这样将列表的元素拆分为多个变量:

1
2
3
4
5
6
my_list = [1, 2, 3]
a, b, c = my_list

# a -> 1
# b -> 2
# c -> 3

但是你知道吗,当你有一个任意长度的列表时,你可以使用星号(*)来赋值给变量?

假设,你想要特定列表变量中的第一个和最后一个元素。

你可以像这样使用星号:

1
2
my_list = [1, 2, 3]
a, *b, c = my_list

在这个示例中,a 现在饮食列表的第一个元素,c 包含 my_list 的最后一个元素。但是,b 中有什么呢?

b 中,有整个列表,除了第一个和最后一个元素,即 [2]。注意,现在 b 是一个列表。

当我们研究更大的列表时,这个概念可能会更清晰一点:

1
2
3
4
5
6
my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a, *b, c = my_list

# a -> 1
# b -> [2, 3, 4, 5, 6, 7, 8, 9]
# c -> 10