Python zip() 函数
Summary
zip 的核心在于 * 的逆向操作,它让 zip 同时具备 " 打包 " 和 " 解压 " 两种能力,这种自逆性比较少见。
1. 打包(正常用法)
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
pairs = list(zip(names, ages))
# [('Alice', 25), ('Bob', 30), ('Charlie', 35)]
2. 解压(* 的巧妙之处)
# 星号 * 把 pairs "展开"为多个参数
# zip(('Alice', 25), ('Bob', 30), ('Charlie', 35))
names2, ages2 = zip(*pairs)
print(names2) # ('Alice', 'Bob', 'Charlie')
print(ages2) # (25, 30, 35)
为什么能这样?
zip(a, b, c)接受多个可迭代对象,横向取元素*把[(a1,b1), (a2,b2)]变成(a1,b1), (a2,b2)两个独立参数- 于是
zip重新横向对齐,第一列归第一列,第二列归第二列
3. 矩阵转置(最经典的 zip(*matrix))
这是线性代数中转置操作的一行代码实现:
matrix = [
[1, 2, 3], # 第0行
[4, 5, 6], # 第1行
[7, 8, 9] # 第2行
]
# zip(*matrix) 相当于 zip([1,2,3], [4,5,6], [7,8,9])
transposed = list(zip(*matrix))
# 结果:第0行变第0列,第1行变第1列...
# [(1, 4, 7), ← 原第0列
# (2, 5, 8), ← 原第1列
# (3, 6, 9)] ← 原第2列
可视化理解:
原始 *解包展开 zip重新组合
1 2 3 → 1 2 3 → 1 4 7
4 5 6 4 5 6 2 5 8
7 8 9 7 8 9 3 6 9
实用场景:
# 读取CSV后,把行数据转为列数据方便分析
rows = [
['Name', 'Age', 'City'], # 表头
['Alice', '25', 'NY'], # 数据1
['Bob', '30', 'LA'] # 数据2
]
# 解压后:每列成为一个元组
columns = list(zip(*rows))
# [('Name', 'Alice', 'Bob'), ('Age', '25', '30'), ('City', 'NY', 'LA')]
# 轻松提取字段
names = columns[0][1:] # ('Alice', 'Bob')
ages = columns[1][1:] # ('25', '30')
三、数据分块(Chunking)技巧
这是 zip 最晦涩但实用的用法,利用 * 和 iter 的配合:
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
n = 3 # 每3个一组
# zip(*[iter(data)]*n) 的解析见下方
chunks = list(zip(*[iter(data)]*n))
# [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
拆解这个黑魔法:
| 步骤 | 代码 | 结果 |
|---|---|---|
| 1 | iter(data) | 创建一个迭代器对象 it |
| 2 | [iter(data)]*n | 列表乘法,得到 [it, it, it](同一个迭代器的 3 个引用) |
| 3 | zip(*[it, it, it]) | 相当于 zip(it, it, it) |
| 4 | zip 取值 | 第 1 个 it 取走 1,第 2 个 it 取走 2,第 3 个 it 取走 3 → 元组 (1,2,3) |
| 5 | 下一轮 | 第 1 个 it 取走 4... 形成 (4,5,6) |
关键:三个 it 是同一个迭代器的引用,zip 横向取时,实际上是在轮流消耗同一个迭代器。
不足 n 个的丢弃问题:
data = [1, 2, 3, 4, 5]
list(zip(*[iter(data)]*3)) # [(1, 2, 3)] —— 4,5 被丢弃
补齐版本(使用 zip_longest):
from itertools import zip_longest
data = [1, 2, 3, 4, 5]
list(zip_longest(*[iter(data)]*3, fillvalue=None))
# [(1, 2, 3), (4, 5, None)]
四、多序列并行处理
zip 让多序列遍历代码变得简洁:
1. 同时修改多个列表(传统 vs zip)
# 传统写法 —— 索引混乱,容易出错
for i in range(len(names)):
print(f"{names[i]}: {ages[i]} - {cities[i]}")
# zip 写法 —— 结构化解构,一目了然
for name, age, city in zip(names, ages, cities):
print(f"{name}: {age} - {city}")
2. 字典多键值批量更新
keys = ['a', 'b', 'c']
values = [1, 2, 3]
# 结合 dict 构造
d = dict(zip(keys, values)) # {'a': 1, 'b': 2, 'c': 3}
# 或更新现有字典
for k, v in zip(keys, values):
d[k] = v * 2
3. 查找差异(对比两个序列)
list1 = [1, 2, 3, 4]
list2 = [1, 2, 4, 4]
# 逐位对比
diff = [(a, b) for a, b in zip(list1, list2) if a != b]
# [(3, 4)] —— 第3个位置不同
五、不等长序列处理
| 需求 | 方案 | 代码 |
|---|---|---|
| 截短至最短(默认) | zip | zip(a, b) |
| 填充至最长 | zip_longest | zip_longest(a, b, fillvalue=0) |
| 仅处理等长(严格模式) | Python 3.10+ strict | zip(a, b, strict=True) |
严格模式(Python 3.10+):
# 若长度不等立即报错,防止静默截断导致的数据丢失
for x, y in zip([1, 2], [1, 2, 3], strict=True):
pass
# ValueError: zip() argument 2 is longer than argument 1
六、记忆口诀
"* 解包是钥匙,zip 既能封也能拆 "
" 一行转置 zip(*M),分块 *[iter]*n 要记牢 "
最复杂的 zip(*[iter(data)]*n) 理解要点:列表里的 n 个迭代器是同一个对象的引用,zip 横向取数时,实际上是在串行消耗同一个数据源。