0%

揭秘 Python args 和 kwargs (译)

本文用于练习英文阅读,如有侵权,联系删除

原文链接Davide MastromatteoPython args and kwargs: Demystified

以下为译文
有时,当你在查看Python函数定义的时候,你可能会看到这两个奇怪的参数: *args 和 **kwargs 。如果你想知道这两个特殊的变量名是什么,或者你的IDE(集成开发环境)为什么在main()函数里定义他们,这篇文章适合你,你将学到如何使用argskwargs让你的Python函数定义更灵活。

读完这篇文章,你将了解:

  • *args 和 **kwargs 的实际意思
  • 如何使用 *args 和 **kwargs 定义函数
  • 如何使用单星号(*) 解包迭代
  • 如何使用双星号(**) 解包字典

本文假设你已经知道如何定义Python函数和列表字典

将多个参数传递给函数

*args 和 **kwargs 允许你将多个参数或关键字参数传给一个字典。思考下面这个例子。这是一个给定两个参数返回他们的和的简单函数。

1
2
3

def my_sum(a, b):
return a + b

这个函数工作正常,但是他只能接收两个参数。如果你需要对不同数量的参数进行计算,如果传递的参数的个数仅在运行时才能确定,该怎么办?创建一个不管传递多少整数都可以顺利求和的函数会更好。

在函数定义中使用 Python args变量

有几种方法可以帮你传递不同数量的参数给函数。对于有一些集合经验的人来说,第一种方式最符合直觉。你住需要简单的将List(列表)或者Set(集合)所有的参数传递给你的函数。因此,对于my_sum(),你可以传递需要添加的所有整数的列表:

1
2
3
4
5
6
7
8
9
# sum_integers_list.py
def my_sum(my_integers):
result = 0
for x in my_integers:
result += x
return result

list_of_integers = [1, 2, 3]
print(my_sum(list_of_integers))

它可以有效的工作,但是你每次调用这个函数都需要创建一个列表传进来。这很不方便,尤其是你可能并不能事先知道所有需要放入列表的值。

这是*args非常有用的地方,因为它允许你传递不同数量的位置参数,示例如下:

1
2
3
4
5
6
7
8
9
# sum_integers_args.py
def my_sum(*args):
result = 0
# Iterating over the Python args tuple
for x in args:
result += x
return result

print(my_sum(1, 2, 3))

在这个例子里,你不再传递列表给my_sum().相反,你传递了3个不同的位置参数。my_sum获取所有提供的参数所谓输入打包他们放到一个可迭代对象 args中。

注意,args只是一个名称,你可以不使用args作为名称。你可以选择任何你喜欢的名字,像integers:

1
2
3
4
5
6
7
8
# sum_integers_args_2.py
def my_sum(*integers):
result = 0
for x in integers:
result += x
return result

print(my_sum(1, 2, 3))

即使你将可迭代对象integers而不是args传递过去,这个函数仍然有效。这里最重要的是你要使用解包操作符(*)。

要记住,用解包操作获取的可迭代对象不是List(列表)而是一个元组(tuple)。元组类似与列表,他们都支持切片和迭代。但是元组有一个非常不同的地方,列表是可变的,元组不可变。要测试这一点,可以运行以下代码。下面这个脚本尝试改变列表的值。

1
2
3
my_list = [1, 2, 3]
my_list[0] = 9
print(my_list)

这个位于列表的第一个索引值应该被更新为9。如果你执行这个脚本,你将看到列表确实被修改了。

1
2
$python change_list.py
[9, 2, 3]

第一个值不再是0,被更新成了9.现在尝试用元组做相同的事情。

1
2
3
4
# change_tuple.py
my_tuple = (1, 2, 3)
my_tuple[0] = 9
print(my_tuple)

这里,你将看到同样的值,除了他们是用元组组合在一起。如果你尝试执行这个脚本,你将看到Python解释器返回一个错误:

1
2
3
4
5
$python change_tuple.py
Traceback (most recent call last):
File "change_tuple.py", line 3, in <module>
my_tuple[0] = 9
TypeError: 'tuple' object does not support item assignment

这是因为元组是一个不可变对象,它的值在赋值后不能再被修改。当你使用元组和*args时,请记住这一点。

使用Python kwargs 变量定义函数

好的,现在你已经明白了 *args的用途,但**kwargs呢?**kwargs工作方式和*args 差不多,但是它不接收位

print(concatenate(a=“Real”, b=“Python”, c=“Is”, d=“Great”, e=“!”))

1
2
3
4
当你执行上面脚本,concatenate() 将遍历所有Python kwargs字典 并连接它找到的所有值:
```bash
$python concatenate.py
RealPythonIsGreat!

如同 args ,kwargs 可以改成任何你想改成的值。同样,最重要的是解包操作符(**)。

所以,前面那个例子可以写成下面这种形式:

1
2
3
4
5
6
7
8
# concatenate_2.py
def concatenate(**words):
result = ""
for arg in words.values():
result += arg
return result

print(concatenate(a="Real", b="Python", c="Is", d="Great", e="!"))

注意,上面这个例子迭代一个标准字典对象。如果迭代字典并像上面那样返回所有值,如示例,你必须使用.values()。

事实上,如果你忘了这种使用方法。你将发现自己迭代的是Python kwargs 字典的键,如下示例:

1
2
3
4
5
6
7
8
9
# concatenate_keys.py
def concatenate(**kwargs):
result = ""
# Iterating over the keys of the Python kwargs dictionary
for arg in kwargs:
result += arg
return result

print(concatenate(a="Real", b="Python", c="Is", d="Great", e="!"))

注意,如果你尝试执行这个例子,你将注意到一下输出

1
2
$ python concatenate_keys.py
abcde

如你所见,如果你没有指定.values(),你的函数将迭代Python kwargs字典的键值,返回错误的结果。

函数参数顺序

现在,你学习了*args和**kwargs的用途,你就可以使用它们开始编写传入不同数量参数的函数了,但是如果你想创建一个传入可变数量的位置参数和命名参数,怎么办?

在这种情况下,你必须记住参数顺序,就像非默认参数必须在默认参数前面,*args 也必须在 **kwargs前面。

总结一下,参数的正确顺序:

  1. 标准参数
  2. *args 参数
  3. **kwargs 参数

例如,下面这个函数定义正确:

1
2
3
# correct_function_definition.py
def my_function(a, b, *args, **kwargs):
pass

这个*args 应该在 **kwargs前面列出。但是如果你尝试修改参数的顺序?例如,请思考下面这个函数:

1
2
3
4
# wrong_function_definition.py
def my_function(a, b, **kwargs, *args):
pass

现在,**kwargs 被放在*args 函数定义前面。如果你尝试运行这个例子,解释器将报错:

1
2
3
4
5
$ python wrong_function_definition.py
File "wrong_function_definition.py", line 2
def my_function(a, b, **kwargs, *args):
^
SyntaxError: invalid syntax

在这种情况,*args 被放在**kwargs前面,Python 解释器将报语法错误。

通过星号操作 * & ** 解码

你现在能够用 *args 和 **kwargs 去定义可以传递可变数量参数的Python 函数。让我们来深入了解一下关于解包操作符的更多知识。

在Python2中引入了单星号和双星号解包操作符。从Python 3.5 开始,由于PEP 448而变得更强大。简而言之,解包操作符可以解包获取Python里可以迭代的对象。单星号操作符 * 可以在所有Python提供的可迭代对象中使用,双星号操作符 ** 只能在字典中使用。

让我们看个例子“

1
2
3
# print_list.py
my_list = [1, 2, 3]
print(my_list)

这个代码定义了一个列表并打印到标准输出里:

1
2
$ python print_list.py
[1, 2, 3]

注意,这个列表的打印方式,以及相应的括号和逗号。

现在,尝试将解包操作符 * 放到你的列表名前面:

1
2
3
# print_unpacked_list.py
my_list = [1, 2, 3]
print(*my_list)

在这里,解包运算符* 告诉print() 先解包这个列表
在这种情况,输出不再是列表本身,而是列表的内容:

1
2
$ python print_unpacked_list.py
1 2 3

你可以看出这个与之前执行的print_list.py的区别么?
除了列表外,print() 还使用了三个单独的参数做为输入。

你会注意到另一个事实是 ,在print_unpakced_list.py中 ,你是用解包操作符* 去调用函数,而不是在定义函数中,在这种情况 print() 会将列表中所有元素视为单个独立的参数。

你也可以使用这个方法调用你自己的函数,但是如果你的函要求特定数量的参数,这个可解包的迭代对象必须拥有同样数量的参数。

为了测试这个行为,考虑这个脚本:

1
2
3
4
5
6
# unpacking_call.py
def my_sum(a, b, c):
print(a + b + c)

my_list = [1, 2, 3]
my_sum(*my_list)

这里,my_sum() 明确指出要求a,b,c三个参数。
如果你运行这个脚本,你将得到my_list 里这三个数的和:

1
2
$ python unpacking_call.py
6

my_list 的三个元素和my_sum 要求的参数正好匹配。

现在让我们跟随这个脚本,当我的my_list 里有4个参数而不是3个会怎样:

1
2
3
4
5
6
# wrong_unpacking_call.py
def my_sum(a, b, c):
print(a + b + c)

my_list = [1, 2, 3, 4]
my_sum(*my_list)

这个例子,my_sum 仍然只需要3个参数,但是* 操作给了4个元素,如果你尝试执行这个脚本,你将看到Python 解释器无法运行它:

1
2
3
4
5
$ python wrong_unpacking_call.py
Traceback (most recent call last):
File "wrong_unpacking_call.py", line 6, in <module>
my_sum(*my_list)
TypeError: my_sum() takes 3 positional arguments but 4 were given

当你使用*操作符去解包一个列表并作为参数传递给函数时,就像你要单独把每一个参数传递进去一样。这意味着你可以使用多个解包操作符去从几个列表中获取值并把他们一起传递给一个函数。

为了测试这个特性,思考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
# sum_integers_args_3.py
def my_sum(*args):
result = 0
for x in args:
result += x
return result

list1 = [1, 2, 3]
list2 = [4, 5]
list3 = [6, 7, 8, 9]

print(my_sum(*list1, *list2, *list3))

如果你运行这个例子,这3个列表都被解包。每一个元素都被传递到my_sum(),从而产生下面这个输出:

1
2
$ python sum_integers_args_3.py
45

解包操作符还有其他实用用法。例如,结舌你需要把一个列表分割成三个不同部分,这个输出应该显示第一个值,最后一个值,和中间所有的值。通过解包操作符号,你可以只用一行代码做到:

1
2
3
4
5
6
7
8
9
# extract_list_body.py
my_list = [1, 2, 3, 4, 5, 6]

a, *b, c = my_list

print(a)
print(b)
print(c)

在这个例子,my_list 包含6个元素。第一个值分配给a,最后一个值分配给c,剩下所有其他元素分配给新列表b。如果你运行这个脚本,print() 将如你预期的打印这3个变量:

1
2
3
4
$ python extract_list_body.py
1
[2, 3, 4, 5]
6

使用解包操作符* 可以做的另一件事就是分割任何可以迭代的对象。如果你需要合并两个列表,这可能非常有用:

1
2
3
4
5
6
# merging_lists.py
my_first_list = [1, 2, 3]
my_second_list = [4, 5, 6]
my_merged_list = [*my_first_list, *my_second_list]

print(my_merged_list)

解包操作符* 被放在 my_first_list 和my_second_list 前面:

如果你运行这个脚本,看到的结果是列表被合并了:

1
2
$ python merging_lists.py
[1, 2, 3, 4, 5, 6]

你也可以用使用解包操作符**去合并两个字典:

1
2
3
4
5
6
# merging_dicts.py
my_first_dict = {"A": 1, "B": 2}
my_second_dict = {"C": 3, "D": 4}
my_merged_dict = {**my_first_dict, **my_second_dict}

print(my_merged_dict)

在这里可迭代对象是 my_first_dict 和 my_second_dict。

执行这个代码输出合并字典:

1
2
$ python merging_dicts.py
{'A': 1, 'B': 2, 'C': 3, 'D': 4}

记住这个*操作符可以用于任何可迭代对象,它也可以迭代一个字符串。

1
2
3
# string_to_list.py
a = [*"RealPython"]
print(a)

在Python里,字符串也是一个可迭代对象,所以*将解包所有单个值放在列表a中:

1
2
$ python string_to_list.py
['R', 'e', 'a', 'l', 'P', 'y', 't', 'h', 'o', 'n']

前面的示例看起来不错,但是当你使用这些运算符时,请务必记住Tim Peters撰写的《Python之禅》的第七条:可读性很重要。

要了解原因,请思考以下代码:

1
2
3
# mysterious_statement.py
*a, = "RealPython"
print(a)

有解包运算符*,后面跟着变量,逗号,和赋值号。被打包成一行!事实上,这和之前那个例子没有什么不同。只是RealPython字符串被赋值到新列表a中,感谢解包操作符*。

a后面的逗号可以解决问题,当你使用解包操作给变量赋值时,Python要求你的结果变量是列表或元组。逗号结尾,你实际只定义了一个变量名为a的元组。

尽管这是一个非常巧妙的技巧,很多Pythonistas(更Python风格的使用者)都不认为这样的代码有很好的可读性,因此,最好谨慎使用这种数据结构。

结论

你现在可以使用 *args和 **kwargs去接受可变数量的参数在你的函数里。你也了解了更多关于解包操作符的知识。

你已经了解了:

  • *args 和**kwargs 的含义
  • 如何使用 *args 和 **kwargs 定义函数
  • 如何使用单星号(*)解包可迭代对象
  • 如何使用双星号(**)解包字典

欢迎关注我的其它发布渠道