Python 解包:深入解析
Python 是一种广泛应用的编程语言。本文将深入探讨其一个核心却常被忽视的特性:Python 中的解包操作。
你可能在别人的代码中见过 *
和 **
,甚至在不完全理解其作用的情况下使用过。本文将详细解释解包的概念,并演示如何运用它编写更具 Python 风格的代码。
以下是一些在学习本文时有帮助的概念:
- 可迭代对象:任何能被
for
循环遍历的序列,如集合、列表、元组和字典。 - 可调用对象:可以使用双括号
()
调用的 Python 对象,例如myfunction()
。 - Shell:一个交互式运行环境,允许我们执行 Python 代码。可以通过在终端输入
python
来启动。 - 变量:用于存储对象,并具有内存位置的符号名称。
首先,我们来澄清一个常见的混淆点:在 Python 中,星号 *
也用作算术运算符。单个星号用于乘法,而双星号 **
则表示求幂运算。
>>> 3*3 9 >>> 3**3 27
可以通过启动 Python shell 并输入上述代码来验证这一点。
注意:你需要安装 Python 3 才能学习本文。如果你尚未安装,请查阅 Python 安装指南。
如你所见,我们在第一个数字和第二个数字之间使用了星号。这种用法表示我们正在进行算术运算。
>>> *range(1, 6), (1, 2, 3, 4, 5) >>> {**{'vanilla':3, 'chocolate':2}, 'strawberry':2} {'vanilla': 3, 'chocolate': 2, 'strawberry': 2}
另一方面,当我们在可迭代对象前使用星号 (*
, **
) 时,表示我们要对其进行解包,例如:
若你尚未完全理解,不必担心,这只是 Python 解包的引言,请继续阅读完整教程。
什么是解包?
解包是指提取某个容器内的元素的过程,这些容器包括列表、元组和字典等。可以将其想象成打开一个盒子,从中取出不同的物品,如电缆、耳机或 USB 设备。
在 Python 中进行解包类似于现实生活中打开一个盒子。
>>> mybox = ['cables', 'headphones', 'USB'] >>> item1, item2, item3 = mybox
下面将同一个例子用代码进行展示,以便更好地理解:
正如你所见,我们将 mybox
列表中的三个元素分别赋值给了 item1
、item2
和 item3
三个变量。这种变量赋值是 Python 解包的基本概念。
>>> item1 'cables' >>> item2 'headphones' >>> item3 'USB'
如果你尝试获取每个变量的值,你会发现 item1
指向 ‘cables’,item2
指向 ‘headphones’,以此类推。
>>> newbox = ['cables', 'headphones', 'USB', 'mouse'] >>> item1, item2, item3 = newbox Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: too many values to unpack (expected 3)
到目前为止,代码运行正常,但如果我们要解包一个含有更多元素的列表,并保持赋值变量的数量不变,该如何处理?
这正是你可能预料到的错误。本质上,我们尝试将 4 个列表项赋值给 3 个变量,Python 如何正确分配值?实际上,我们得到了一个值错误(ValueError),
错误信息是 “too many values to unpack”,表示要解包的值太多。 出现此错误是因为我们在左侧设置了 3 个变量,而在右侧(newbox 列表)有 4 个值。
>>> lastbox = ['cables', 'headphones'] >>> item1, item2, item3 = lastbox Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: not enough values to unpack (expected 3, got 2)
如果进行类似的操作,尝试解包的变量多于值,则会得到另一个 ValueErro,错误信息略有不同:
注意: 我们一直在处理列表,但这种形式的解包适用于任何可迭代对象(列表、集合、元组、字典)。
那么,如何解决这种情况呢?是否有方法可以将可迭代对象的所有项解包到有限的变量中,而不产生任何错误?
当然有,这就是解包运算符或星号运算符 (*
, **
) 的作用。让我们看看如何在 Python 中使用它。
如何使用 *
运算符解包列表
星号运算符用于解包尚未被分配的可迭代对象中的所有值。
>>> first, *unused, last = [1, 2, 3, 5, 7] >>> first 1 >>> last 7 >>> unused [2, 3, 5]
>>> first, *_, last = [1, 2, 3, 5, 7] >>> _ [2, 3, 5]
假设你想在不使用索引的情况下获取列表的第一个和最后一个元素,我们可以使用星号运算符来实现:
>>> first, *_, last = [1, 2] >>> first 1 >>> last 2 >>> _ []
正如你所见,我们使用星号运算符来获取所有未使用的值。 丢弃值的首选方法是使用下划线变量 _
,它有时被用作“虚拟变量”。
即使列表只有两个元素,我们仍然可以使用此技巧:
在这种情况下,下划线变量(虚拟变量)存储一个空列表,因此其他两个变量仍然可以访问列表中的可用值。
>>> *string = 'PythonIsTheBest'
常见问题排除
>>> *string = 'PythonIsTheBest' File "<stdin>", line 1 SyntaxError: starred assignment target must be in a list or tuple
我们可以解包可迭代对象中的唯一元素。 你可能会想到这样的操作:但上面的代码会返回一个 SyntaxError,因为根据 PEP规范:
简单赋值左侧必须是元组(或列表)。
>>> *string, = 'PythonIsTheBest' >>> string ['P', 'y', 't', 'h', 'o', 'n', 'I', 's', 'T', 'h', 'e', 'B', 'e', 's', 't']
>>> *numbers, = range(5) >>> numbers [0, 1, 2, 3, 4]
如果我们想将可迭代对象的所有值解包到一个变量中,我们必须创建一个元组,添加一个简单的逗号即可:
另一个例子是使用 range
函数,该函数会返回一个数字序列。
现在你已经知道如何使用星号来解包列表和元组,是时候开始解包字典了。
如何使用 **
运算符解包字典
>>> **greetings, = {'hello': 'HELLO', 'bye':'BYE'} ... SyntaxError: invalid syntax
单个星号 *
用于解包列表和元组,而双星号 **
则用于解包字典。
>>> food = {'fish':3, 'meat':5, 'pasta':9} >>> colors = {'red': 'intensity', 'yellow':'happiness'} >>> merged_dict = {**food, **colors} >>> merged_dict {'fish': 3, 'meat': 5, 'pasta': 9, 'red': 'intensity', 'yellow': 'happiness'}
不幸的是,我们不能像处理元组和列表那样将字典解包为单个变量。这意味着以下操作会引发错误:
但是,我们可以在可调用对象和其他字典中使用 **
运算符。例如,如果我们想创建一个由其他字典组成的合并字典,可以使用下面的代码:
这是创建复合字典的一种非常简洁的方式,但这并非在 Python 中进行解包的主要方式。
接下来,我们来看看如何使用可调用对象的解包功能。
封装在函数中:args
和 kwargs
在类或函数实现中,你可能已经见过 args
和 kwargs
。让我们看看为什么我们需要将它们与可调用对象一起使用。
>>> def product(n1, n2): ... return n1 * n2 ... >>> numbers = [12, 1] >>> product(*numbers) 12
使用 *
运算符打包 (args
)
>>> product(12, 1) 12
假设我们有一个计算两个数字乘积的函数。
>>> numbers = [12, 1, 3, 4] >>> product(*numbers) ... TypeError: product() takes 2 positional arguments but 4 were given
如你所见,我们将一个列表解包到函数中,因此我们实际执行的是以下命令:
>>> def product(*args): ... result = 1 ... for i in args: ... result *= i ... return result ... >>> product(*numbers) 144
到目前为止,一切正常,但是如果我们要传递一个更长的列表呢?肯定会引发错误,因为函数接收的参数数量超过了它能处理的范围。
我们可以通过直接将列表打包到函数中来解决所有这些问题,这将创建一个可迭代对象,允许我们将任意数量的参数传递给函数。
在这里,我们将 args
参数视为一个可迭代对象,遍历其元素并返回所有数字的乘积。请注意结果的起始数字必须为 1,因为如果我们从零开始,函数将始终返回零。注意:args
只是一个约定,你可以使用任何其他参数名称。我们也可以在不使用列表的情况下将任意数字传递给函数,就像内置的 print
函数一样。
>>> product(5, 5, 5) 125 >>> print(5, 5, 5) 5 5 5
.
最后,我们来获取函数参数的对象类型。
>>> def test_type(*args): ... print(type(args)) ... print(args) ... >>> test_type(1, 2, 4, 'a string') <class 'tuple'> (1, 2, 4, 'a string')
正如上面的代码所示,args
的类型始终是元组,其内容是传递给函数的所有非关键字参数。
使用 **
运算符打包 (kwargs
)
>>> def make_person(name, **kwargs): ... result = name + ': ' ... for key, value in kwargs.items(): ... result += f'{key} = {value}, ' ... return result ... >>> make_person('Melissa', id=12112, location='london', net_worth=12000) 'Melissa: id = 12112, location = london, net_worth = 12000, '
正如我们之前看到的,**
运算符专门用于字典。这意味着使用此运算符,我们能够将键值对作为参数传递给函数。
让我们创建一个 make_person
函数,它接收一个位置参数 name
和不定数量的关键字参数。
如你所见,**kwargs
语句将所有关键字参数转换为一个字典,我们可以在函数内部对其进行迭代。
>>> def test_kwargs(**kwargs): ... print(type(kwargs)) ... print(kwargs) ... >>> test_kwargs(random=12, parameters=21) <class 'dict'> {'random': 12, 'parameters': 21}
注意:kwargs
只是一个约定,你可以随意命名该参数。
我们可以像检查 args
一样检查 kwargs
的类型:
>>> def my_final_function(*args, **kwargs): ... print('Type args: ', type(args)) ... print('args: ', args) ... print('Type kwargs: ', type(kwargs)) ... print('kwargs: ', kwargs) ... >>> my_final_function('Python', 'The', 'Best', language="Python", users="A lot") Type args: <class 'tuple'> args: ('Python', 'The', 'Best') Type kwargs: <class 'dict'> kwargs: {'language': 'Python', 'users': 'A lot'}
kwargs
内部变量总是会变成一个字典,存储传递给函数的键值对。
最后,我们来看在同一个函数中同时使用 args
和 kwargs
的示例:
结论
- 解包运算符在日常任务中非常有用,现在你已经了解了如何在单个语句和函数参数中使用它们。
- 在本教程中,你学习了:
- 使用
*
表示元组和列表,使用**
表示字典。 - 可以在函数和类构造函数中使用解包运算符。
args
用于将非关键字参数传递给函数,而 kwargs
用于将关键字参数传递给函数。