Packing and unpacking in Python (2)
前言
本文是该系列第二篇文章,将介绍一下对函数参数的拆包和装包的巧妙使用方式,并通过一些实例,说明需要注意的事项。
本文中的命令行示范,都是基于以下环境:
- 操作系统: CentOS 7
- Python:
3.7.03.7.9 (更新)
函数参数的拆包和装包
拆包和装包更广泛使用的场合,是在函数的参数传递过程中。
实参拆包
有时,我们定义了一个函数,它接受多个形参,而在实际调用时,调用者想传递的参数,都在一个可迭代对象或一个字典中,这时候可以用单个星号(*)
把实参中的可迭代对象、或者用双星号(**)
把实参中的字典,拆包成函数形参对应的值,这个过程叫实参拆包。实参拆包的语法是:
|
|
上述语句是函数调用,args 和 kwargs 是实参,args 必须是一个可迭代对象,kwargs 是一个字典。 Python 会先把 args 对象中的元素依次按位置赋值给函数的形参,这些就是位置参数(positionial argument)
,然后 Python 再把 kwargs 中的 key/value 元素按关键字赋值给函数剩下的形参,这些就是关键字参数(keyword argument)
。赋值给形参的过程中,要保证元素数量与函数参数个数一致,并且语句没有歧义,否则会报错。来看例子:
|
|
执行结果是:
|
|
例子中通过单星号和双星号能成功把 list 和 dict 拆包成函数的实参。最后一个例子报错,是因为在 my_list2 拆包后,分别把它的元素赋值给了形参 a 和 b,而 my_dict2 拆包时,又有了形参 b,导致形参 b 有多个值。这句代码相当于:
|
|
如果能理解 Python 函数参数的赋值方法,就能理解这里的错误。
双星号
,如果是用单星号的话,会把字典的键当做位置参数拆包。如果上述例子中第三次拆包的代码如下:
|
|
那么执行结果就是:
|
|
实参拆包的另一个巧用
实参拆包的另一个巧用是用于合并两个可迭代对象。直接看例子:
|
|
tuple1 和 tuple2 被拆包后,直接构造了新的 tuple 对象,这里利用了 tuple 的构造函数。合并两个 tuple 也可以用更简单的写法:
|
|
因为 tuple 类用__add__
方法,重构了加法操作,所以这种写法和上面效果一样。但是,当你想合并两个不同类型的对象的时候,就有区别了:
|
|
执行结果:
|
|
第一个合并能够成功,因为拆包可以作用于任何可迭代对象,所以无论是 tuple 还是 list 对象,都可以被拆包并把元素用于构建新的 tuple。第二个合并失败了,因为 tuple 重构的加法操作无法让自己和一个非 tuple 对象做加法。这个例子体现了用拆包的优势。
形参装包
形参装包有点像是实参拆包的反向操作。定义一个函数,它接受一个带星号的形参,当调用者想传递多个实参的时候,Python 便会把实参放在一个列表或字典里,传递给形参。形参用单星号(*)
修饰,是一个 tuple 对象,装载位置参数(positionial argument)
。形参用双星号(**)
修饰,是一个 dict 对象,装载关键字参数(keyword argument)
。赋值的过程中,要保证语句没有歧义。来看例子:
|
|
执行结果:
|
|
上述例子中,当调用 fun() 函数时,Python 将实参逐个赋值给形参,先是把 1 赋给 a,接着后面的形参带单星号,Python 就把位置参数 2 和 3 装载 tuple 对象里,赋给了 args,最后剩下一个关键字参数 c,Python 将它转义成一个字典,赋给了 kwargs。
需要注意,形参和实参都需要遵守 Python 的参数定义规范,函数声明的形参需要先定义单星号,再定义双星号。调用函数时的实参需要先传递位置参数,再传递关键字参数,如果位置颠倒,Python 会报错:
|
|
执行:
|
|
拆包和装包的混合使用
上一节介绍的实参拆包和形参装包,在很多场合,可以混合使用,十分灵活。来看一个例子:
|
|
在看结果前,尝试自己思考执行的结果会是什么样,尤其是两处调用 fun2() 的地方,会打印什么信息,有什么区别。
这里的代码将拆包和装包混合使用,遇到这类稍微复杂些的代码,牢记一下三点即可:
- 在函数调用处,遇到单星号的实参(可迭代对象),将其内容原地展开,遇到双星号的实参(字典),将其按照关键字参数的格式原地展开。
- 在函数声明及定义内,单星号的形参 args 一定是个 tuple 对象,双星号的形参 kwargs 一定是个 dict 对象。
- 从函数调用出进入函数体内,实参逐个赋给形参,直到遇到形参 *args 或 **kwargs,这时将多余的位置参数放入 tuple 对象赋给 args,多余的关键字参数放入字典对象赋给 kwargs。
围绕这两点,依次执行代码,就可以搞懂代码如何运行。比如,上面的例子可以分为以下几步:
-
在 fun1() 函数调用处,把单星号实参 my_list 原地展开,得到:
1
fun1(1, 2, 3, 4, c='test1', d='test2')
-
进入 fun1() 函数体,实参逐个赋给形参,a = 1,b = 2,这时形参只剩下单星号 args 和双星号 kwargs了,将多余的位置参数 3 和 4 放入元祖并赋给 args,多余的关键字参数 c=‘test1’ 和 d=‘test2’ 装入字典并赋给 kwargs。所以此时:
1 2
args = (3, 4) kwargs = {'c': 'test1', 'd': 'test2'}
-
逐个执行打印语句:
1 2 3 4 5
a is 1 b is 2 args is (3, 4) kwargs is {'c': 'test1', 'd': 'test2'} Try to unpack args
-
对于 print(*args) 函数调用,把单星号实参 args 原地展开,得到:
1
print(3, 4)
所以打印结果:
1
3 4
-
在 fun2() 函数调用处,由于实参没有星号,所以按照正常调用,把 args 和 kwargs 的值代入即可:
1
fun2((3, 4), {'c': 'test1', 'd': 'test2'})
进入 fun2() 函数体内,由于 fun2 函数的第一个形参就是 *args,所以将位置参数放入元祖并赋给 args。这里要注意的是,{‘c’: ‘test1’, ’d': ‘test2’} 是一个字典对象,它是位置参数,而不是关键字参数(不是 c=‘test1’, d=‘test2’),所以它和 (3, 4) 一样,放入元祖 args 元祖中,所以执行后 args 和 kwargs 为:
1 2
args = ((3, 4), {'c': 'test1', 'd': 'test2'}) kwargs = {}
-
在第二次调用 fun2() 时,实参带了星号,所以将它们展开,相当于:
1
fun2(3, 4, c='test1', d='test2')
进入 fun2() 函数体内,由于 fun2 函数的第一个形参就是 *args,所以将位置参数 3 和 4 放入元祖并赋给 args,关键字参数 c 和 d 放入字典并赋给 kwargs,所以:
1 2
args = (3, 4) kwargs = {'c': 'test1', 'd': 'test2'}
将以上步骤的结果放在一起,就得到了这段代码的正确输出结果:
|
|
把这个例子搞懂,可以帮助你更好地理解 Python 的拆包和装包在函数调用中的使用,也能帮助你写出更灵活的代码。
参考
https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/
「 您的赞赏是激励我创作和分享的最大动力! 」
- 原文链接:https://zhuyinjun.me/2019/python_unpacking_and_packing_2/
- 版权声明:本创作采用 CC BY-NC 4.0 国际许可协议,非商业性使用可以转载,但请注明出处(作者、链接),商业性使用请联系作者获得授权。