目录

Packing and unpacking in Python (2)

前言

本文是该系列第二篇文章,将介绍一下对函数参数的拆包和装包的巧妙使用方式,并通过一些实例,说明需要注意的事项。

注意

本文中的命令行示范,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python: 3.7.0 3.7.9 (更新)

 

函数参数的拆包和装包

拆包和装包更广泛使用的场合,是在函数的参数传递过程中。

实参拆包

有时,我们定义了一个函数,它接受多个形参,而在实际调用时,调用者想传递的参数,都在一个可迭代对象或一个字典中,这时候可以用单个星号(*)把实参中的可迭代对象、或者用双星号(**)把实参中的字典,拆包成函数形参对应的值,这个过程叫实参拆包。实参拆包的语法是:

1
fun(*args, **kwargs)

上述语句是函数调用,args 和 kwargs 是实参,args 必须是一个可迭代对象,kwargs 是一个字典。 Python 会先把 args 对象中的元素依次按位置赋值给函数的形参,这些就是位置参数(positionial argument),然后 Python 再把 kwargs 中的 key/value 元素按关键字赋值给函数剩下的形参,这些就是关键字参数(keyword argument)。赋值给形参的过程中,要保证元素数量与函数参数个数一致,并且语句没有歧义,否则会报错。来看例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def fun(a, b, c, d):
  print(f"the argument passed to me is: {a} {b} {c} {d}")

my_list1 = [1, 2, 3, 4]
my_iterator = range(4)
fun(*my_list1)		# unpacking the list's elements to fun's argument
fun(*my_iterator)		# unpacking the iterator's elements to fun's argument

my_list2 = [1, 2]
my_dict = {'c': 3, 'd': 4}
fun(*my_list2, **my_dict)		# unpacking list's element and dict's key/value pair to fun's argument

my_dict2 = {'b': 3, 'c': 4}
fun(*my_list2, **my_dict2)		# error

执行结果是:

1
2
3
4
5
6
7
the argument passed to me is: 1 2 3 4
the argument passed to me is: 0 1 2 3
the argument passed to me is: 1 2 3 4
Traceback (most recent call last):
  File "test_unpacking_and_packing.py", line 14, in <module>
    fun(*my_list2, **my_dict2)
TypeError: fun() got multiple values for argument 'b'

例子中通过单星号和双星号能成功把 list 和 dict 拆包成函数的实参。最后一个例子报错,是因为在 my_list2 拆包后,分别把它的元素赋值给了形参 a 和 b,而 my_dict2 拆包时,又有了形参 b,导致形参 b 有多个值。这句代码相当于:

1
fun(1, 2, b=3, c=4)

如果能理解 Python 函数参数的赋值方法,就能理解这里的错误。

注意
需要注意的是,把字典对象拆包成关键字参数,需要在字典对象前用双星号,如果是用单星号的话,会把字典的键当做位置参数拆包。

如果上述例子中第三次拆包的代码如下:

1
fun(*my_list2, *my_dict)

那么执行结果就是:

1
the argument passed to me is: 1 2 c d

实参拆包的另一个巧用

实参拆包的另一个巧用是用于合并两个可迭代对象。直接看例子:

1
2
3
4
tuple1 = (1, 2, 3)
tuple2 = (4, 5)
merged_tuple = (*tuple1, *tuple2)
print(merged_tuple)

tuple1 和 tuple2 被拆包后,直接构造了新的 tuple 对象,这里利用了 tuple 的构造函数。合并两个 tuple 也可以用更简单的写法:

1
merged_tuple = tuple1 + tuple2

因为 tuple 类用__add__方法,重构了加法操作,所以这种写法和上面效果一样。但是,当你想合并两个不同类型的对象的时候,就有区别了:

1
2
3
4
5
6
tuple1 = (1, 2, 3)
list1 = [4, 5]
merged_tuple1 = (*tuple1, *list1)
print(merged_tuple1)
merged_tuple2 = tuple1 + list1
print(merged_tuple2)

执行结果:

1
2
3
(1, 2, 3, 4, 5)
...
TypeError: can only concatenate tuple (not "list") to tuple

第一个合并能够成功,因为拆包可以作用于任何可迭代对象,所以无论是 tuple 还是 list 对象,都可以被拆包并把元素用于构建新的 tuple。第二个合并失败了,因为 tuple 重构的加法操作无法让自己和一个非 tuple 对象做加法。这个例子体现了用拆包的优势。

形参装包

形参装包有点像是实参拆包的反向操作。定义一个函数,它接受一个带星号的形参,当调用者想传递多个实参的时候,Python 便会把实参放在一个列表或字典里,传递给形参。形参用单星号(*)修饰,是一个 tuple 对象,装载位置参数(positionial argument)。形参用双星号(**)修饰,是一个 dict 对象,装载关键字参数(keyword argument)。赋值的过程中,要保证语句没有歧义。来看例子:

1
2
3
4
5
6
def fun(a, *args, **kwargs):
  print(f"the argument a is {a}")
  print(f"the argument args is {args}")
  print(f"the argument kwargs is {kwargs}")

fun(1, 2, 3, c='test')

执行结果:

1
2
3
the argument a is 1
the argument args is (2, 3)
the argument kwargs is {'c': 'test'}

上述例子中,当调用 fun() 函数时,Python 将实参逐个赋值给形参,先是把 1 赋给 a,接着后面的形参带单星号,Python 就把位置参数 2 和 3 装载 tuple 对象里,赋给了 args,最后剩下一个关键字参数 c,Python 将它转义成一个字典,赋给了 kwargs。

需要注意,形参和实参都需要遵守 Python 的参数定义规范,函数声明的形参需要先定义单星号,再定义双星号。调用函数时的实参需要先传递位置参数,再传递关键字参数,如果位置颠倒,Python 会报错:

1
fun(1, 2, c='test', 3)

执行:

1
SyntaxError: positional argument follows keyword argument

 

拆包和装包的混合使用

上一节介绍的实参拆包和形参装包,在很多场合,可以混合使用,十分灵活。来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def fun1(a, b, *args, **kwargs):
  print(f"a is {a}")
  print(f"b is {b}")
  print(f"args is {args}")
  print(f"kwargs is {kwargs}")
  print("Try to unpack args")
  print(*args)      	# unpack args
  print("Pass original args, kwargs to fun2")
  fun2(args, kwargs)		# original args, kwargs
  print("Pass unpacked args, kwargs to fun2")
  fun2(*args, **kwargs)			# unpack args, kwargs

def fun2(*args, **kwargs):
  print(f"args in fun2 function is {args}")
  print(f"kwargs in fun2 function is {kwargs}")

my_list = [1, 2]
fun1(*my_list, 3, 4, c='test1', d='test2')

在看结果前,尝试自己思考执行的结果会是什么样,尤其是两处调用 fun2() 的地方,会打印什么信息,有什么区别。

这里的代码将拆包和装包混合使用,遇到这类稍微复杂些的代码,牢记一下三点即可:

  • 在函数调用处,遇到单星号的实参(可迭代对象),将其内容原地展开,遇到双星号的实参(字典),将其按照关键字参数的格式原地展开。
  • 在函数声明及定义内,单星号的形参 args 一定是个 tuple 对象,双星号的形参 kwargs 一定是个 dict 对象。
  • 从函数调用出进入函数体内,实参逐个赋给形参,直到遇到形参 *args 或 **kwargs,这时将多余的位置参数放入 tuple 对象赋给 args,多余的关键字参数放入字典对象赋给 kwargs。

围绕这两点,依次执行代码,就可以搞懂代码如何运行。比如,上面的例子可以分为以下几步:

  1. 在 fun1() 函数调用处,把单星号实参 my_list 原地展开,得到:

    1
    
    fun1(1, 2, 3, 4, c='test1', d='test2')
    
  2. 进入 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'}
    
  3. 逐个执行打印语句:

    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
    
  4. 对于 print(*args) 函数调用,把单星号实参 args 原地展开,得到:

    1
    
    print(3, 4)
    

    所以打印结果:

    1
    
    3 4
    
  5. 在 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 = {}
    
  6. 在第二次调用 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'}
    

将以上步骤的结果放在一起,就得到了这段代码的正确输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
a is 1
b is 2
args is (3, 4)
kwargs is {'c': 'test1', 'd': 'test2'}
Try to unpack args
3 4
Pass original args, kwargs to fun2
args in fun2 function is ((3, 4), {'c': 'test1', 'd': 'test2'})
kwargs in fun2 function is {}
Pass unpacked args, kwargs to fun2
args in fun2 function is (3, 4)
kwargs in fun2 function is {'c': 'test1', 'd': 'test2'}

把这个例子搞懂,可以帮助你更好地理解 Python 的拆包和装包在函数调用中的使用,也能帮助你写出更灵活的代码。

 

参考

https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」