📖 来源:Effective Python 编写高质量 Python 代码的 90 个有效方法,条款 6
很多刚上手 Python 的同学,访问元组、列表里的数据时,第一反应就是疯狂写下标[0]、[1]、[i‑1]。下标写多了,代码里到处都是数字魔法值,可读性直线下降,改代码的时候还很容易把[0]写成[1],bug 悄咪咪就埋进了项目里😵。
Python 给我们提供了一把 "神兵利器"------拆包(unpacking,也叫解包)。一行代码就可以把可迭代对象里面的值直接分配给变量,大幅减少下标访问,写出地道的 Pythonic 风格代码。
Bilibili 同步视频
一、先搞懂:元组不能改,但拆包不是修改元组
元组tuple大家都知道,是不可变序列。一旦创建完成,就不能通过下标修改内部元素。
python
# 定义元组
item = ("peanut butter", "jelly")
# 通过下标取值
first = item[0]
second = item[1]
print(first, "and", second)
# 尝试修改元组,直接报错
pair = ("chocolate", "peanut butter")
pair[0] = "honey"
运行后会抛出:TypeError: 'tuple' object does not support item assignment,元组本身拒绝原地修改。
⚠️这里很多新手会产生误解:拆包并不是修改元组本身! 拆包只是把元组内部的值复制出来,赋值给外面全新的变量,这些变量是可读可写的,原始元组纹丝不动。
python
item = ("peanut butter", "jelly")
# ✨拆包unpacking,一行完成赋值
first, second = item
print(first, "and", second)
输出:
Plain
peanut butter and jelly
对比两种写法:
-
下标写法:先写
item[0]、item[1],还要额外定义变量,代码啰嗦;如果元组元素数量变了,下标全部要手动改。 -
拆包写法:一眼就知道这个元组包含
first、second两个含义,代码简洁,意图一目了然。
💡小知识:拆包不局限于元组!列表、字典 items、生成器,所有可迭代对象 iterable都支持拆包,甚至支持嵌套拆包。
嵌套拆包:处理多层嵌套数据
Python 支持深度嵌套的拆包语法,哪怕数据一层套一层,也可以一次性解析出来。
⚠️提示:这种写法语法合法,但现实业务不要过度嵌套,可读性会反向崩盘,了解语法即可。
python
favorite_snacks = {
'salty': ('pretzels', 100),
'sweet': ('cookies', 180),
'veggie': ('carrots', 20),
}
# 嵌套拆包,直接解析字典items的key和内层元组
((type1, (name1, cal1)),
(type2, (name2, cal2)),
(type3, (name3, cal3))) = favorite_snacks.items()
print(f"Favorite {type1} is {name1} with {cal1} calories")
print(f"Favorite {type2} is {name2} with {cal2} calories")
print(f"Favorite {type3} is {name3} with {cal3} calories")
输出:
Plain
Favorite salty is pretzels with 100 calories
Favorite sweet is cookies with 180 calories
Favorite veggie is carrots with 20 calories
二、经典骚操作:拆包实现变量原地交换 🎯
写算法的时候,交换两个变量,传统思维需要搞一个临时temp中转变量,这是其他编程语言非常常见的写法。
我们拿冒泡排序举例,传统临时变量交换版本:
python
def bubble_sort(a):
for _ in range(len(a)):
for i in range(1, len(a)):
if a[i] < a[i-1]:
# 传统三行交换,借助临时temp
temp = a[i]
a[i] = a[i-1]
a[i-1] = temp
names = ['pretzels', 'carrots', 'arugula', 'bacon']
bubble_sort(names)
print(names)
输出:
Plain
['arugula', 'bacon', 'carrots', 'pretzels']
而借助拆包语法,一行就完成交换,干掉 temp 临时变量!
python
def bubble_sort(a):
for _ in range(len(a)):
for i in range(1, len(a)):
if a[i] < a[i-1]:
# ✨拆包交换,仅此一行
a[i-1], a[i] = a[i], a[i-1]
names = ['pretzels', 'carrots', 'arugula', 'bacon']
bubble_sort(names)
print(names)
✅运行结果完全一致。很多同学好奇,这行魔法代码底层发生了什么?
-
Python 优先计算赋值符号
=右边 :先把a[i]、a[i‑1]打包,生成一个临时匿名元组(a[i], a[i‑1]); -
然后对这个临时元组执行拆包,依次赋值给等号左边的
a[i‑1]、a[i]; -
赋值完成后临时元组直接被垃圾回收销毁,我们完全感知不到它的存在。
⚡性能小提示:会产生临时元组,但开销极小,日常业务完全不用顾虑性能,代码清晰度优先。只有超大规模极致性能的热点循环,才需要考虑临时变量写法,绝大多数场景直接用拆包。
三、循环迭代:告别下标遍历,优雅解析复合数据
我们经常遇到这样的数据:列表里面每一个元素都是元组,比如[(零食名,卡路里), ...]。
很多新手会写出下面这种 "C 语言风格" Python 代码,用range(len())配合下标一点点抠数据:
python
snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)]
# ❌不推荐:大量下标,代码冗长,阅读费劲
for i in range(len(snacks)):
item = snacks[i]
name = item[0]
calories = item[1]
print(f"#{i+1}: {name} has {calories} calories")
代码可以跑,但观感很差,到处都是[0]、[1],如果以后元组增加字段,下标全部要修改,极易出错。
结合enumerate + 循环内拆包,一键变身地道 Pythonic 写法:
python
snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)]
# ✅推荐!enumerate拿序号,直接拆包元组
for rank, (name, calories) in enumerate(snacks, 1):
print(f"#{rank}: {name} has {calories} calories")
输出完全一样:
Plain
#1: bacon has 350 calories
#2: donut has 240 calories
#3: muffin has 190 calories
✨优势总结:
-
不再需要下标索引,变量名
name、calories直接表达业务含义; -
enumerate(snacks, 1)直接从 1 开始生成排名序号,不用手动写i+1; -
逻辑和数据结构对齐,别人读代码一眼看懂每个字段代表什么。
拓展:拆包的使用场景非常广,不止循环。列表构建、函数多返回值接收、函数参数、关键字参数传参都可以用上拆包语法。
📝核心要点总结
-
拆包 (unpacking) 是 Python 专属语法:一行把可迭代对象的多个值分配给多个变量,减少下标索引。
-
拆包不等于修改原始容器:元组本身不可变,只是把值拷贝赋值给外部变量,原始元组保持原样。
-
支持嵌套拆包,但不要写过度嵌套,会牺牲可读性。
-
经典玩法:
a, b = b, a一行交换变量,底层生成临时元组,业务场景放心使用。 -
循环遍历复合序列,优先用
enumerate搭配拆包,拒绝range(len(xxx))+ 下标访问。 -
列表、元组、字典 items、生成器,所有可迭代对象,都支持拆包语法。
✍️写在最后
下标索引不是不能用,但滥用下标会让 Python 代码变得啰嗦、脆弱。合理用好 unpacking 拆包,写出来的代码更贴近 Python 的设计哲学,可读性直接上一个台阶。 当然凡事过犹不及,不要为了炫技写七八层嵌套拆包,清晰可读永远是第一准则。

参考书籍:Effective Python 编写高质量 Python 代码的 90 个有效方法 条款 6