📜  在Python中解包:并行分配之外

📅  最后修改于: 2020-08-31 13:18:47             🧑  作者: Mango

介绍

Python中的拆包是指通过在单个赋值语句中将可迭代的值赋给变量元组(或list)组成的操作。作为补充,当我们使用可迭代的拆包运算符收集单个变量中的多个值时,可以使用术语打包*

从历史上看,Python开发人员通常将这种操作称为元组拆包。但是,由于该Python功能已被证明非常有用和流行,因此已被广泛应用于各种可迭代对象。如今,一个更现代,更准确的术语将是反复包装

在本教程中,我们将学习什么是可迭代的解包以及如何利用此Python功能使我们的代码更具可读性,可维护性和pythonic。

另外,我们还将介绍一些实际示例,这些示例说明如何在赋值操作,for循环,函数定义和函数调用的上下文中使用可迭代的拆包功能。

用Python打包和解包

Python允许一个tuple(或list)变量出现在赋值操作的左侧。中的每个变量都tuple可以*从赋值右侧的一个可迭代对象接收一个值(如果使用运算符,则可以接收多个)。

由于历史原因,Python开发人员过去常将此元组称为unpacking。但是,由于此功能已推广到所有可迭代的类型,因此更准确的术语是可迭代解包,这就是本教程中将要称呼的那个。

拆包操作在Python开发人员中非常流行,因为它们可以使我们的代码更具可读性和雅致性。让我们仔细看看如何用Python解压缩,看看这个功能如何改善我们的代码。

打开元组

在Python中,我们可以将a tuple变量放在赋值运算符(=)的左侧,并将a tuple值放在右侧。右边的值将根据它们在中的位置自动分配给左边的变量tuple。这在Python中通常称为元组解包。查看以下示例: 

>>> (a, b, c) = (1, 2, 3)
>>> a
1
>>> b
2
>>> c
3

当将元组放在赋值运算符的两侧时,将进行元组拆包操作。右侧的值根据每个变量的相对位置分配给左侧的变量tuple。如您在上面的示例中所见,awill be 1bwill be 2cwill be 3

要创建一个tuple对象,我们不需要使用一对括号()作为分隔符。这也适用于元组拆包,因此以下语法是等效的:

>>> (a, b, c) = 1, 2, 3
>>> a, b, c = (1, 2, 3)
>>> a, b, c = 1, 2, 3

由于所有这些变体都是有效的Python语法,因此我们可以根据情况使用其中的任何一种。可以说,在Python中解压缩时,更常使用最后一种语法。

当我们使用元组拆包将值拆包为变量时,左侧的变量数tuple必须与右侧的值数完全匹配tuple。否则,我们会得到一个ValueError

例如,在下面的代码中,我们在左侧使用两个变量,在右侧使用三个值。这将ValueError告诉我们,有太多值需要解压缩: 

>>> a, b = 1, 2, 3
Traceback (most recent call last):
  ...
ValueError: too many values to unpack (expected 2)

注意:唯一的例外是当我们使用*运算符将多个值打包到一个变量中时,我们将在后面介绍。

另一方面,如果我们使用的变量多于值,那么我们将得到一个,ValueError但是这一次消息表明没有足够的值要解压:

>>> a, b, c = 1, 2
Traceback (most recent call last):
  ...
ValueError: not enough values to unpack (expected 3, got 2)

如果在元组拆包操作中使用不同数量的变量和值,则将得到ValueError。这是因为Python需要明确地知道什么值进入了什么变量,因此它可以进行相应的赋值。

打开迭代器

元组解包功能在Python开发人员中非常流行,以至于语法已扩展为可与任何可迭代对象一起使用。唯一的要求是,可迭代对象在接收tuple(或list)中每个变量恰好产生一个项目。

请查看以下示例,了解如何在Python中进行迭代拆包:

>>> # Unpacking strings
>>> a, b, c = '123'
>>> a
'1'
>>> b
'2'
>>> c
'3'
>>> # Unpacking lists
>>> a, b, c = [1, 2, 3]
>>> a
1
>>> b
2
>>> c
3
>>> # Unpacking generators
>>> gen = (i ** 2 for i in range(3))
>>> a, b, c = gen
>>> a
0
>>> b
1
>>> c
4
>>> # Unpacking dictionaries (keys, values, and items)
>>> my_dict = {'one': 1, 'two':2, 'three': 3}
>>> a, b, c = my_dict  # Unpack keys
>>> a
'one'
>>> b
'two'
>>> c
'three'
>>> a, b, c = my_dict.values()  # Unpack values
>>> a
1
>>> b
2
>>> c
3
>>> a, b, c = my_dict.items()  # Unpacking key-value pairs
>>> a
('one', 1)
>>> b
('two', 2)
>>> c
('three', 3)

在Python中解包时,我们可以在赋值运算符的右侧使用任何可迭代的函数。左侧可以用tuple或填充list变量。看看下面的示例,其中我们tuple在赋值语句的右侧使用了:

>>> [a, b, c] = 1, 2, 3
>>> a
1
>>> b
2
>>> c
3

如果使用range()迭代器,则其工作方式相同:

>>> x, y, z = range(3)
>>> x
0
>>> y
1
>>> z
2

尽管这是一种有效的Python语法,但它在实际代码中并不常用,对于初学者Python开发人员可能有些困惑。

最后,我们还可以set在拆包操作中使用对象。但是,由于集合是无序集合,因此分配的顺序可能不连贯,并可能导致细微的错误。查看以下示例:

>>> a, b, c = {'a', 'b', 'c'}
>>> a
'c'
>>> b
'b'
>>> c
'a'

如果我们在拆包操作中使用集合,则分配的最终顺序可能与我们想要和期望的完全不同。因此,最好避免在拆包操作中使用集合,除非分配顺序对我们的代码不重要。

用*运算符打包

在这种情况下,该*运算符称为元组(或可迭代)拆包运算符。它扩展了拆包功能,使我们可以在单个变量中收集或打包多个值。在下面的示例中,我们tuple使用*运算符将值a 打包到单个变量中: 

>>> *a, = 1, 2
>>> a
[1, 2]

为了使此代码正常工作,分配的左侧必须为tuple(或a list)。这就是为什么我们使用结尾逗号。它tuple可以包含所需数量的变量。但是,它只能包含一个加星标的表达式

*就像*a上面的代码中的一样,我们可以使用拆包运算符和有效的Python标识符形成一个加注星标的表达式。左侧的其余变量tuple称为强制变量,因为它们必须用具体的值填充,否则,我们会得到一个错误。这是实际的工作方式。

将尾随值打包在中b

>>> a, *b = 1, 2, 3
>>> a
1
>>> b
[2, 3]

将起始值打包在中a

>>> *a, b = 1, 2, 3
>>> a
[1, 2]
>>> b
3

a因为bc强制包装一个值:

>>> *a, b, c = 1, 2, 3
>>> a
[1]
>>> b
2
>>> c
3

在包装的任何值a(a默认[]),因为bcd是强制性的:

>>> *a, b, c, d = 1, 2, 3
>>> a
[]
>>> b
1
>>> c
2
>>> d
3

没有为强制变量(e)提供值,因此发生错误:

>>> *a, b, c, d, e = 1, 2, 3
 ...
ValueError: not enough values to unpack (expected at least 4, got 3)

*当我们需要在不使用list()函数的情况下将生成器的元素收集到单个变量中时,用运算符将值打包到一个变量中会很方便。在以下示例中,我们使用*运算符将生成器表达式的元素和范围对象打包到单个变量中: 

>>> gen = (2 ** x for x in range(10))
>>> gen
 at 0x7f44613ebcf0>
>>> *g, = gen
>>> g
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
>>> ran = range(10)
>>> *r, = ran
>>> r
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

在这些示例中,*操作者在包装中的元素gen,并ran进入gr分别。使用他的语法,我们无需调用从对象,生成器表达式或生成器函数list()创建list值的a range

请注意,我们不能在*没有向赋值左侧的变量添加尾部逗号的情况下,使用unpacking运算符将多个值打包到一个变量中。因此,以下代码将不起作用:

>>> *r = range(10)
  File "", line 1
SyntaxError: starred assignment target must be in a list or tuple

如果尝试使用*运算符将多个值打包到单个变量中,则需要使用单例tuple语法。例如,要使上面的示例起作用,我们只需要在变量之后添加一个逗号即可r,例如in *r, = range(10)

在实践中使用打包和拆包

打包和拆包操作在实践中可能非常有用。它们可以使您的代码清晰,可读和Pythonic。让我们看一下Python中打包和解压缩的一些常见用例。

并行分配

在Python中最常见的解包用例之一就是所谓的并行分配。并行分配使您可以在单个简洁的语句中将可迭代的值分配给变量tuple(或list)。

例如,假设我们有一个关于公司员工的数据库,我们需要将列表中的每个项目分配给一个描述性变量。如果我们忽略了Python中可迭代的拆包工作原理,那么我们可以自己编写如下代码:

>>> employee = ["John Doe", "40", "Software Engineer"]
>>> name = employee[0]
>>> age = employee[1]
>>> job = employee[2]
>>> name
'John Doe'
>>> age
'40'
>>> job
'Software Engineer'

即使此代码有效,索引处理也可能笨拙,难以键入且令人困惑。可以使用以下代码编写更干净,更易读的pythonic解决方案:

>>> name, age, job = ["John Doe", "40", "Software Engineer"]
>>> name
'John Doe'
>>> age
40
>>> job
'Software Engineer'

在Python中使用解压缩,我们可以通过一个简单,简洁而优雅的语句解决上一个示例的问题。这种微小的变化将使我们的代码对于新手开发人员来说更易于阅读和理解。

在变量之间交换值

Python中解压缩的另一个出色应用是在变量之间交换值,而无需使用临时或辅助变量。例如,假设我们需要交换两个变量a和的值b。为此,我们可以坚持使用传统解决方案,并使用一个临时变量来存储要交换的值,如下所示:

>>> a = 100
>>> b = 200
>>> temp = a
>>> a = b
>>> b = temp
>>> a
200
>>> b
100

此过程需要三个步骤和一个新的临时变量。如果我们在Python中使用解压缩,那么我们可以在一个简单的步骤中获得相同的结果:

>>> a = 100
>>> b = 200
>>> a, b = b, a
>>> a
200
>>> b
100

在声明中a, b = b, a,我们重新分配abba在一行代码。这更具可读性和直接性。另外,请注意,使用此技术,不需要新的临时变量。

使用*收集多个值

当我们使用某些算法时,在某些情况下,我们需要将可迭代值或序列的值分成几部分值以进行进一步处理。以下示例显示如何使用list切片操作来执行此操作: 

>>> seq = [1, 2, 3, 4]
>>> first, body, last = seq[0], seq[1:3], seq[-1]
>>> first, body, last
(1, [2, 3], 4)
>>> first
1
>>> body
[2, 3]
>>> last
4

即使此代码可以按我们期望的那样工作,但处理索引和分片可能会有些烦人,难以阅读并且对初学者造成混乱。它还具有使代码僵化且难以维护的缺点。在这种情况下,可迭代的拆包运算符*和可以将多个值打包到单个变量中的功能可能是一个很好的工具。查看上面代码的重构: 

>>> seq = [1, 2, 3, 4]
>>> first, *body, last = seq
>>> first, body, last
(1, [2, 3], 4)
>>> first
1
>>> body
[2, 3]
>>> last
4

这条线first, *body, last = seq使这里神奇。可迭代拆包经营者,*,聚集在中间的元素seqbody。这使我们的代码更具可读性,可维护性和灵活性。您可能在想,为什么更灵活?好吧,假设它seq改变了道路的长度,您仍然需要收集中的中间元素body。在这种情况下,由于我们在Python中使用解包,因此无需更改代码即可正常工作。看看这个例子: 

>>> seq = [1, 2, 3, 4, 5, 6]
>>> first, *body, last = seq
>>> first, body, last
(1, [2, 3, 4, 5], 6)

如果我们在Python中使用序列切片而不是可迭代的解包,那么我们将需要更新索引和切片以正确捕获新值。

如果*Python可以明确确定要分配给每个变量的元素,则可以将操作符用于在单个变量中打包多个值的操作可以用于多种配置。看下面的例子:

>>> *head, a, b = range(5)
>>> head, a, b
([0, 1, 2], 3, 4)
>>> a, *body, b = range(5)
>>> a, body, b
(0, [1, 2, 3], 4)
>>> a, b, *tail = range(5)
>>> a, b, tail
(0, 1, [2, 3, 4])

我们可以将*运算符移到变量tuple(或list)中,以根据需要收集值。唯一的条件是Python可以确定将哪个变量赋给每个值。

重要的是要注意,在赋值中不能使用多个星号表达式。如果这样做,我们将得到SyntaxError如下所示的结果:>>> *a, *b = range(5) File , line 1 SyntaxError: two starred expressions in assignment

>>> *a, *b = range(5)
  File "", line 1
SyntaxError: two starred expressions in assignment

如果*在赋值表达式中使用两个或多个,则将SyntaxError告诉我们找到了两个星号的表达式。之所以这样,是因为Python不能明确确定我们要分配给每个变量的值。

用*删除不需要的值

*运算符的另一个常见用例是将其与虚拟变量名一起使用,以删除一些无用或不需要的值。查看以下示例:

>>> a, b, *_ = 1, 2, 0, 0, 0, 0
>>> a
1
>>> b
2
>>> _
[0, 0, 0, 0]

对于这个用例的更深刻的例子,假设我们正在开发一个脚本,该脚本需要确定我们正在使用的Python版本。为此,我们可以使用sys.version_infoattribute。此属性返回包含的版本号的五个组件的元组:majorminormicroreleaselevel,和serial。但是我们只需要majorminormicro,脚本才能正常工作,因此我们可以删除其余部分。这是一个例子:

>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=8, micro=1, releaselevel='final', serial=0)
>>> mayor, minor, micro, *_ = sys.version_info
>>> mayor, minor, micro
(3, 8, 1)

现在,我们有了所需信息的三个新变量。其余信息存储在伪变量中_,我们的程序可以忽略该变量。这可以向新手开发人员明确,我们不想(或不需要)使用存储在其中的信息,_因为此字符没有明显的含义。

注意:默认情况下,_Python解释器使用下划线字符存储我们在交互式会话中运行的语句的结果值。因此,在这种情况下,使用此字符来标识虚拟变量可能是模棱两可的。

返回函数中的元组

Python函数可以返回几个用逗号分隔的值。由于我们可以在tuple不使用括号的情况下定义对象,因此这种操作可以解释为返回tuple值a。如果我们对返回多个值的函数进行编码,则可以对返回的值执行可迭代的打包和拆包操作。

看看下面的示例,其中我们定义了一个函数来计算给定数字的平方和立方:

>>> def powers(number):
...     return number, number ** 2, number ** 3
...
>>> # Packing returned values in a tuple
>>> result = powers(2)
>>> result
(2, 4, 8)
>>> # Unpacking returned values to multiple variables
>>> number, square, cube = powers(2)
>>> number
2
>>> square
4
>>> cube
8
>>> *_, cube = powers(2)
>>> cube
8

如果我们定义一个返回逗号分隔值的函数,那么我们可以对这些值进行任何打包或拆包操作。

使用*运算符合并可迭代对象

拆包运算符另一个有趣的用例*是能够将多个可迭代对象合并为最终序列的功能。此功能适用于列表,元组和集合。看下面的例子:

>>> my_tuple = (1, 2, 3)
>>> (0, *my_tuple, 4)
(0, 1, 2, 3, 4)
>>> my_list = [1, 2, 3]
>>> [0, *my_list, 4]
[0, 1, 2, 3, 4]
>>> my_set = {1, 2, 3}
>>> {0, *my_set, 4}
{0, 1, 2, 3, 4}
>>> [*my_set, *my_list, *my_tuple, *range(1, 4)]
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> my_str = "123"
>>> [*my_set, *my_list, *my_tuple, *range(1, 4), *my_str]
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, '1', '2', '3']

*在定义序列时,可以使用可迭代的拆包运算符,将子序列(或可迭代的)的元素拆包为最终序列。这将允许我们在不调用类似的方法创建从其他现有的序列飞序列append()insert()等。

最后两个示例表明,这也是连接可迭代对象的更易读和有效的方式。list(my_set) + my_list + list(my_tuple) + list(range(1, 4)) + list(my_str)我们只写而不是写[*my_set, *my_list, *my_tuple, *range(1, 4), *my_str]

使用**运算符解开字典

在Python中解包的情况下,该**运算符称为字典解包运算符PEP 448扩展了此运算符的使用。现在,我们可以在函数调用使用它,在内涵和发电机的表达,并在显示器

字典解包运算符的基本用例是使用单个表达式将多个字典合并为一个最终字典。让我们看看它是如何工作的: 

>>> numbers = {"one": 1, "two": 2, "three": 3}
>>> letters = {"a": "A", "b": "B", "c": "C"}
>>> combination = {**numbers, **letters}
>>> combination
{'one': 1, 'two': 2, 'three': 3, 'a': 'A', 'b': 'B', 'c': 'C'}

如果我们在字典显示中使用字典解包运算符,则可以对字典进行解包并将其组合以创建最终字典,该字典包含原始字典的键值对,就像在上面的代码中所做的那样。

需要注意的重要一点是,如果我们要合并的字典具有重复键或公用键,那么最右边的字典的值将覆盖最左边的字典的值。这是一个例子:

>>> letters = {"a": "A", "b": "B", "c": "C"}
>>> vowels = {"a": "a", "e": "e", "i": "i", "o": "o", "u": "u"}
>>> {**letters, **vowels}
{'a': 'a', 'b': 'B', 'c': 'C', 'e': 'e', 'i': 'i', 'o': 'o', 'u': 'u'}

 

由于a密钥在两个字典中都存在,因此占主导的值来自vowels,这是最右边的字典。发生这种情况是因为Python开始从左到右添加键/值对。如果在此过程中,Python查找到已经存在的键,则解释器将使用新值更新该键。这就是a在上面的示例中键的值小写的原因。

开箱即用

我们还可以在for循环的上下文中使用可迭代的拆包。当我们运行for循环时,循环在每次迭代中将其可迭代项分配给目标变量。如果要分配的项目是可迭代的,则可以使用tuple目标变量。该循环会将手边的可迭代tuple对象解压缩为目标变量。

例如,假设我们有一个包含有关公司销售数据的文件,如下所示:

产品 价钱 售出单位
铅笔 0.25 1500
笔记本 1.30 550
橡皮 0.75 1000

从此表中,我们可以构建一个list包含两个元素的元组。每个都tuple将包含产品名称,价格和已售单位。有了这些信息,我们想计算每种产品的收入。为此,我们可以使用如下for循环:

>>> sales = [("Pencil", 0.22, 1500), ("Notebook", 1.30, 550), ("Eraser", 0.75, 1000)]
>>> for item in sales:
...     print(f"Income for {item[0]} is: {item[1] * item[2]}")
...
Income for Pencil is: 330.0
Income for Notebook is: 715.0
Income for Eraser is: 750.0

此代码按预期方式工作。但是,我们使用索引来访问每个元素的各个元素tuple。新手开发人员可能很难阅读和理解。

让我们看一下在Python中使用解压缩的另一种实现:

>>> for product, price, sold_units in sales:
...     print(f"Income for {product} is: {price * sold_units}")
...
Income for Pencil is: 330.0
Income for Notebook is: 715.0
Income for Eraser is: 750.0

我们现在在for循环中使用可迭代的拆包。这使我们的代码更具可读性和可维护性,因为我们使用描述性名称来标识每个元素tuple。这一微小的变化将使新来的开发人员能够快速了解​​代码背后的逻辑。

也可以*for循环中使用运算符,以将多个项目打包到单个目标变量中: 

>>> for first, *rest in [(1, 2, 3), (4, 5, 6, 7)]:
...     print("First:", first)
...     print("Rest:", rest)
...
First: 1
Rest: [2, 3]
First: 4
Rest: [5, 6, 7]

在此for循环中,我们将捕获中每个序列的第一个元素first。然后,*运算符list在其目标变量中捕获a 值rest

最后,目标变量的结构必须与可迭代的结构一致。否则,我们会得到一个错误。看下面的例子:

>>> data = [((1, 2), 2), ((2, 3), 3)]
>>> for (a, b), c in data:
...     print(a, b, c)
...
1 2 2
2 3 3
>>> for a, b, c in data:
...     print(a, b, c)
...
Traceback (most recent call last):
  ...
ValueError: not enough values to unpack (expected 3, got 2)

在第一个循环中,目标变量(a, b), c的结构与可迭代项的结构一致((1, 2), 2)。在这种情况下,循环将按预期工作。相反,第二个循环使用目标变量的结构,该目标变量的结构与iterable中各项的结构不同,因此该循环失败并引发a ValueError

函数的打包和解包

在定义和调用函数时,我们还可以使用Python的打包和解包功能。这是在Python中打包和解压缩非常有用且流行的用例。

在本节中,我们将介绍在函数定义或函数调用中如何在Python函数中使用打包和解包的基础知识。

注意:有关这些主题的更深入,更详尽的材料,请使用*args和来**kwargs检查Python中的可变长度参数

用*和**定义函数

我们可以在Python函数的签名中使用*and **运算符。这将允许我们使用可变数量的位置实参(*)或可变数量的关键字实参或两者来调用函数。让我们考虑以下函数:

>>> def func(required, *args, **kwargs):
...     print(required)
...     print(args)
...     print(kwargs)
...
>>> func("Welcome to...", 1, 2, 3, site='imangodoc.com')
Welcome to...
(1, 2, 3)
{'site': 'imangodoc.com'}

上面的函数至少需要一个称为的参数required。它也可以接受可变数量的位置和关键字参数。在这种情况下,*在一个叫做元组操作员收集或包额外的位置参数args**一个叫字典运营商收集或包额外的关键字参数kwargs。两者,args并且kwargs是可选的,自动默认(){}分别。

即使名称argskwargs被Python社区广泛使用,它们也不是使这些技术起作用的必要条件。语法仅要求***后面跟随有效标识符。因此,如果您可以为这些参数指定有意义的名称,则可以这样做。那肯定会提高代码的可读性。

用*和**调用函数

在调用函数时,我们还可以受益于使用*and **运算符将参数集合分别解压缩为单独的位置或关键字参数。这与函数的使用***签名相反。在签名中,运算符的意思是在一个标识符中收集或打包可变数量的参数。在通话时,他们指解压可迭代成几个参数。

这是一个如何工作的基本示例:

>>> def func(welcome, to, site):
...     print(welcome, to, site)
...
>>> func(*["Welcome", "to"], **{"site": 'imangodoc.com'})
Welcome to imangodoc.com

在这里,*操作员将序列解压缩成["Welcome", "to"]位置参数。同样,**运算符将字典解压缩为名称与解压缩字典的键匹配的参数。

我们还可以将这一技术与上一节中介绍的技术相结合,以编写非常灵活的函数。这是一个例子:

>>> def func(required, *args, **kwargs):
...     print(required)
...     print(args)
...     print(kwargs)
...
>>> func("Welcome to...", *(1, 2, 3), **{"site": 'imangodoc.com'})
Welcome to...
(1, 2, 3)
{'site': 'imangodoc.com'}

在定义和调用Python函数时,使用*and **运算符将为其赋予额外的功能,并使它们更加灵活和强大。

 

结论

可迭代的解压缩事实证明是Python中一个非常有用且流行的功能。此功能使我们可以将迭代器分解为几个变量。另一方面,打包包括使用拆包运算符将多个值捕获到一个变量中*

在本教程中,我们学习了如何在Python中使用可迭代解包来编写更具可读性,可维护性和pythonic的代码。

有了这些知识,我们现在能够在Python中使用可迭代的解包来解决常见的问题,例如并行赋值和在变量之间交换值。我们还可以在其他结构(例如for循环,函数调用和函数定义)中使用此Python功能。