Python 通过带星号的 unpacking 操作来捕获多个元素
拆分(unpacking)是一种特殊的 Python 语法,只需要一行代码,就能把数据结构里面的多个值分别赋给相应的变量。
基本的 unpacking 操作
首先来看看基本的 unpacking 操作。例如,若我们确定某个元组里面只有两个元素,那么就可以直接把这两个元素分别赋给相应的变量,而不用再通过下标去访问。
item = ('Peanut butter', 'jelly')
first, second = item # Unpacking
print(first, 'and', second)
# >>>
# Peanut butter and Je1ly
通过 unpacking 来赋值要比通过下标去访问元组内的元素更清晰,而且这种写法所需的代码量通常比较少。当然,赋值操作的左边除了可以罗列单个变量,也可以写成列表、序列或任意深度的可迭代对象(iterable)。其实还可以通过 unpacking 原地交换两个变量,而不用专门创建临时变量。
unpacking 机制还有一个特别重要的用法,就是可以在 for 循环或者类似的结构(例如推导与生成表达式)里面,把复杂的数据拆分到相关的变量之中。
snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)]
for rank, (name, calories) in enumerate(snacks, 1):
print(f'#{rank}: {name} has {calories} calories')
# >>>
# #1: bacon has 350 calories
# #2: donut has 240 calories
# #3: muffin has 190 calories
首先调用内置的 enumerate 函数获得当前要迭代的元组,然后针对这个元组做 unpacking,这样就可以直接得到具体的 name 与 calories 值了。
基本的 unpacking 操作有一项限制,就是必须提前确定需要拆解的序列的长度。例如,销售汽车的时候,可能会把每辆车的车龄写在一份列表中,然后按照从大到小的顺序排列好。如果试着通过基本的 unpacking 操作获取其中最旧的两辆车,那么程序运行时会出现异常。
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
car_ages_descending = sorted(car_ages, reverse=True)
oldest, second_oldest = car_ages_descending
# >>>
# Traceback.
# ValueError: too many values to unpack(expected 2)
许多人经常通过下标与切片来处理这个问题。例如,可以明确通过下标把最旧和第二旧的那两辆车取出来,然后把其余的车放到另一份列表中。
oldest = car_ages_descending[0]
second_oldest = car_ages_descending[1]
others = car_ages_descending[2:]
print(oldest, second_oldest, others)
# >>>
# 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
这样做没问题,但是下标与切片会让代码看起来很乱。而且,用这种办法把序列中的元素分成多个子集合,其实很容易出错,因为通常容易把下标多写或少写一个位置。例如,若修改了其中一行,但却忘了更新另一行,那就会遇到这种错误。
带星号的 unpacking 操作
上面问题通过带星号的表达式(starred expression)来解决会更好一些,这也是一种 unpacking 操作,它可以把无法由普通变量接收的那些元素全都囊括进去。下面用带星号的 unpacking 操作改写刚才那段代码,这次既不用取下标,也不用做切片。
oldest, second_oldest, *others = car_ages_descending
print(oldest, second_oldest, others)
# >>>
# 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
这样写简短易读,而且不容易出错,因为它不要求我们在修改完其中一个下标之后,还必须记得同步更新其他的下标。这种带星号的表达式可以出现在任意位置,所以它能够捕获序列中的任何一段元素。
oldest, *others, youngest = car_ages_descending
print(oldest, youngest, others)
*others, second_youngest, youngest = car_ages_descending
print(youngest, second_youngest, others)
# >>>
# 20 0 [19, 15, 9, 8, 7, 6, 4, 1]
# 0 1 [20, 19, 15, 9, 8, 7, 6, 4]
只不过,在使用这种写法时,至少要有一个普通的接收变量与它搭配,否则就会出现 SyntaxError。例如不能像下面这样,只使用带星号的表达式而不搭配普通变量。
*others = car_ages_descending
# >>>
# Traceback
# SyntaxError: starred assignment target must be in a list or tuple
另外,对于单层结构来说,同一级里面最多只能出现一次带星号的 unpacking。
first, *middle, *second_middle, last = [1, 2, 3, 4]
# >>>
# Traceback ...
# SyntaxError: two starred expressions in assignment
如果要拆解的结构有很多层,那么同一级的不同部分里面可以各自出现带星号的 unpacking 操作。当然并不推荐这种写法。这里举这样一个例子,是想帮助大家理解这种带星号的表达式可以实现怎样的拆分效果。
car_inventory = {
'Downtown': ('Silver Shadow', 'Pinto', 'DMC'),
'Airport': ('Skyline', 'Viper', 'Gremlin', 'Nova'),
}
((loc1, (best1, *rest1)),
(loc2, (best2, *rest2))) = car_inventory.items()
print(f'Best at {loc1} is {best1}, {len(rest1)} others')
print(f'Best at {loc2} is {best2}, {len(rest2)} others')
# >>>
# Best at Downtown is Silver Shadow, 2 others
# Best at Airport is Skyline, 3 others
带星号的表达式总会形成一份列表实例。如果要拆分的序列里已经没有元素留给它了,那么列表就是空白的。如果能提前确定有待处理的序列里至少会有 N 个元素,那么这项特性就相当有用。
short_list = [1, 2]
first, second, *rest = short_list
print(first, second, rest)
# >>>
# 1 2 []
unpacking 操作也可以用在迭代器上,但是这样写与把数据拆分到多个变量里面的那种基本写法相比,并没有太大优势。例如,我可以先构造长度为 2 的取值范围(range),并把它封装在 it 这个迭代器里,然后将其中的值拆分到 first 与 second 这两个变量里。但这样写还不如直接使用形式相符的静态列表(例如 [1, 2]),那样更简单。
it = iter(range(1, 3))
first, second = it
print(f'{first} and {second}')
# >>>
# 1 and 2
对迭代器做 unpacking 操作的好处,主要体现在带星号的用法上面,它使迭代器的拆分值更清晰。例如,这里有个生成器,每次可以从含有整个一周的汽车订单的 CSV 文件中取出一行数据。
def generate_csv():
yield ('Date', 'Make', 'Model', 'Year', 'Price')
...
可以用下标和切片来处理这个生成器所给出的结果,但这样写需要很多行代码,而且看着比较混乱。
all_csv_rows = list(generate_csv())
header = all_csv_rows[0]
rows = all_csv_rows[1:]
print('CSV Header:', header)
print('Row count: ', len(rows))
# >>>
# CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')
# Row count: 0
利用带星号的 unpacking 操作,我们可以把第一行(表头)单独放在 header 变量里,同时把迭代器所给出的其余内容合起来表示成 rows 变量。这样写就清楚多了。
it = generate_csv()
header, *row = it
print('CSV Header:', header)
print('Row count: ', len(rows))
# >>>
# CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')
# Row count: 0
带星号的这部分总是会形成一份列表,所以要注意,这有可能耗尽计算机的全部内存并导致程序崩溃。首先必须确定系统有足够的内存可以存储拆分出来的结果数据,然后才可以对迭代器使用带星号的 unpacking 操作
Python 的 unpacking 机制可以用在许多方面,例如构建列表、给函数设计参数列表、传递关键字参数、接收多个返回值等。
明智地使用 unpacking 机制,可以实现很多原来必须通过下标才能写出的功能,这让代码变得更加清晰,而且能充分发挥 Python 的优势。