首页 > Python笔记 阅读数:31

Python函数参数的打包和解包(超级详细)

在前面的教程中,我们介绍了两种可变参数的标记方式:利用一个星号*构建一个参数元组;利用两个星号**构建参数字典。

事实上,在函数参数传递过程中,还有一种看似类似实则不然的参数传递方式。说它“类似”,是因为在外观上它也在参数前打上一个星号*。说它“不然”,是因为这种操作的内涵不同:星号*是作用在实参上的;实参是有讲究的,这些实参主要包括列表、元组、集合、字典及其他可迭代对象。

如果在这类实参前面加上一个星号*,那么 Python 解释器就会对这些可迭代对象进行解包(unpacking,亦有文献译作“解压”),然后将解包后的元素一一分配给多个形参。

Python参数打包

说到解包,我们先介绍一下它的反操作——打包(packing),参见如下代码:
In [1]: val = 1, 2, 3, 4
In [2]: type(val)
Out[2]: tuple
In [3]: val
Out[3]: (1, 2, 3, 4)

在输入 In [1] 处,表达式等号的右边分别是四个零散的整型数 1, 2, 3, 4,然后赋值给了 val 对象。通过元组知识的学习,我们知道,Python 将等号右边的四个整型数“打包”成了一个匿名的元组,然后赋值给 val。

另一方面,Python 中变量的类型并不需要事先声明,而是通过赋值得到的。通过赋值操作,将等号右边的变量类型赋给等号左边的对象即可。如此一来,In [1] 处 val 的类型就被定义为一个元组了。

上述判断可从 Out[2] 的输出结果中得到印证。在输出 Out[2] 中,元组的另外一个标志——那对圆括号 ( ),也被 Python 解释器自动加上了。

Python参数解包

现在的问题是,如果我们把元组作为一个整体给分散对象赋值,那么这个打包元组中的元素会被一一解析出来吗?延续前面变量 val 的赋值,请参考如下代码:
In [4]: a, b, c, d = val
In [5]: print(a, b, c, d)
1 2 3 4 
In [6]: type (a) 
Out[6]: int

从输入 In [4] 处可知,通过等号可将右侧的元组 val一一对应赋值给等号左侧的四个变量。在其他编程语言中,一对四的赋值方式通常是不被允许的。但在 Python 语法糖的包装下,上述方式是合法的。在 In [5] 处,通过 print( ) 输出验证,变量 a、b、c、d 的值均可正常输出。

在 In [6] 处,用全局函数 type( ) 测试 a 的类型,可以看出,a 的类型也是正确的 (int),并非 val 的元组类型。我们把这种将可迭代对象的元素分别赋值为分散对象的过程,称为解包。

关于解包,需要注意的有两点。

第一点:被解包的序列中的元素数量必须与赋值符号=左边元素的数量完全一样,否则就会报错。参见如下代码:
In [7]: val = 1,2,3        #val 为一个包含三个元素的元组
In [8]: a, b, c, d = val   #将 val 解包给四个元素,错误!
-----------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-14-m5c78f840c05> in <module>
------> 1 a, b, c, d = val
ValueError: not enough values to unpack (expected 4, got 3)

在 In [7] 处,元组 val 内包含三个元素,分别是 1、2、3。但在 In [8] 处,等号右侧有四个变量(分别是 a、b、c、d)等着被赋值,解包元素的数量不够!因此 Python 解释器会“毫不客气”地指出问题所在:没有足够的值来解包。

第二点:支持解包操作的不仅限于元组,也包括所有可迭代的对象,比如列表、字典等。

于是,我们想知道,这种自动解包的行为能否也在函数参数传递时发生?比如说,如果实参为一个列表或元组,它会自动解包,将其内的元素一一分配给不同的形参吗?想知道答案,请参看如下代码:
In [9]: def fun (a, b, c, d):   #定义带有四个参数的函数 fun ()
   ...:     print(a, b, c, d)
In [10]: my_list = [1, 2, 3, 4] #定义一个包括四个元素的列表
In [11]: fun(my_list)          #以列表为实参调用fun (),发生错误!
Traceback (most recent call last):
    File "<ipython-input-l6-5440eed904b7>n, line 1, in <module>
        fun(my_list)
TypeError: fun() missing 3 required positional arguments: 'b', 'c' and 'd' 

上述代码的 In [9] 处定义了一个函数 fun( ),它有四个形参 a、b、c、d。然后 In [10] 处又定义了一个包含四个元素的列表 my_list。

我们原有的想法是,把 my_list 作为实参,通过解包操作给四个形参赋值,即分别让 a=1、b=2、c=3、d=4。但 In [11] 处的输出结果让我们失望了,Python 系统好像并不认可这种“简单粗暴”的参数解包行为。

那有没有办法让这种参数解包行为成功呢?其实,我们距成功仅一步之遥。类似于可变参数,只需要在可迭代对象前打上一个星号*,一切就都可以完美解决了,参见如下代码:
In [12]: fun(*my_list)   #可迭代对象前的星号*不可缺少。
12 3 4

In [12] 处参数部分的那个星号*,其功能就是将可迭代对象(实参)的每个元素分解成一个个离散的参数,然后分别赋值给对应的形参。

列表可以这么解包,那如果可迭代对象是一个字典,又该如何解包呢?这时在可迭代的实参前添加一个星号*,是否依然可行呢?请参见如下代码:
In [13]: def fun(a, b, c):
  ...:    print(a, b, c)
  ...:
In [14]: d = {'a':2, 'b':4, 'c':10}
In [15]: fun(*d)
a b c
通过上面的输出结果可以看出,程序运行正常,并无语法错误,但输出结果不是我们想要的,以上代码仅仅把形参名输出了。

那该如何修正呢?如同前文讲解的那样,对于由字典构成的可变参数,我们用两个星号**表示,这里对字典的解包,也需要在字典名称前加上两个星号**,示例代码如下:
In [16]: fun(**d) #在字典对象前加两个星号**,正确输出!
2 4 10

相关文章