前言
zip()的名字来自「拉链」:它把多个可迭代对象并排咬合在一起,第 i 次产出「每个对象各自的第 i 个元素」组成的元组。它解决的是「同时遍历多个序列」这类需求——不用再手写下标去对齐两个列表。
三个必须先纠正的误解:
- 以为
zip()返回列表。在 Python 3 里它返回的是迭代器,惰性求值、只能消费一次。Python 2 里它确实返回列表,这也是很多老教程会写错的原因(Python 2 已在 2020 年 1 月 1 日停止维护,iteritems、xrange这些写法都已经移除或改名)。 - 以为两个序列长度不同会报错。默认行为是按最短的那个截断,多余的元素被静默丢弃。想让它报错,得显式传
strict=True。 - 以为
zip()会「复制」数据。它一个元素都不复制,只在你迭代时按需向各个可迭代对象取下一个值。
一、签名与语义
官方签名是:
zip(*iterables, strict=False)文档的描述很精确:zip()返回一个元组的迭代器,第 i 个元组包含每个参数可迭代对象的第 i 个元素。可以把它想成「把行变成列、把列变成行」,也就是矩阵转置。
# 适用于 Python 3.8+
print(list(zip([1, 2, 3], ["a", "b", "c"])))
# [(1, 'a'), (2, 'b'), (3, 'c')]几个容易被忽略的边界:
# 适用于 Python 3.8+
print(list(zip())) # [],不传参得到空迭代器
print(list(zip([1, 2, 3]))) # [(1,), (2,), (3,)],只传一个时是单元素元组zip()是惰性的:元素不会在创建时被处理,直到你真正迭代它(for循环或包一层list())。
二、长度不等怎么办
这是zip()最容易出静默 bug 的地方。默认是「按最短截断」:
# 适用于 Python 3.8+
print(list(zip(range(3), ["fee", "fi", "fo", "fum"])))
# [(0, 'fee'), (1, 'fi'), (2, 'fo')]注意最后那个'fum'被悄悄丢掉了,没有任何提示。如果你确信几个序列应该等长,就应该用strict=True——它需要Python 3.10 及以上(来自 PEP 618):
# 适用于 Python 3.10+
print(list(zip(("a", "b", "c"), (1, 2, 3), strict=True)))
# [('a', 1), ('b', 2), ('c', 3)]长度不一时它会直接抛ValueError,而不是截断:
# 适用于 Python 3.10+
list(zip(range(3), [1, 2, 3, 4], strict=True))Traceback (most recent call last):
...
ValueError: zip() argument 2 is longer than argument 1| 场景 | 写法 | 行为 |
|---|
| 明确知道会不等长,短的为准 | zip(a, b) | 截断到最短 |
| 认为它们应该等长 | zip(a, b, strict=True)(3.10+) | 不等长抛ValueError |
| 需要保留最长的 | zip_longest(itertools) | 用填充值补齐短的一方 |
itertools.zip_longest是标准库里补这一块的工具,它接受fillvalue指定填充值。
三、常见用法
用法一:同时遍历多个序列。
# 适用于 Python 3.8+
names = ["ann", "bob", "cid"]
scores = [95, 88, 90]
for name, score in zip(names, scores):
print(f"{name}: {score}")用法二:用两个序列拼出字典。
# 适用于 Python 3.8+
keys = ["name", "age", "city"]
values = ["ann", 20, "beijing"]
print(dict(zip(keys, values)))
# {'name': 'ann', 'age': 20, 'city': 'beijing'}用法三:转置矩阵。
# 适用于 Python 3.8+
matrix = [
[1, 2, 3],
[4, 5, 6],
]
print(list(zip(*matrix)))
# [(1, 4), (2, 5), (3, 6)]这里的*matrix把三行拆成三个参数传给zip,于是得到三列。
用法四:反向操作——把「元组列表」拆成两个序列。
# 适用于 Python 3.8+
pairs = [(1, "a"), (2, "b"), (3, "c")]
nums, letters = zip(*pairs)
print(nums) # (1, 2, 3)
print(letters) # ('a', 'b', 'c')注意解包出来的是元组,不是列表。
用法五:配合enumerate给结果编号。
# 适用于 Python 3.8+
a = ["x", "y"]
b = [1, 2]
for i, (left, right) in enumerate(zip(a, b), start=1):
print(i, left, right)四、完整示例:对齐两个数据源
一个常见的真实场景:一份是「列名」,另一份是「每行的值」,要把它们拼成字典列表。
# 适用于 Python 3.8+
def rows_to_dicts(columns, rows, strict=True):
"""把列名和每行数据对齐成字典列表。
strict 默认开启,防止列数和值数不一致时被静默截断。"""
result = []
for row in rows:
if len(row) != len(columns):
raise ValueError(f"列数与值数不一致:{len(columns)} vs {len(row)}")
result.append(dict(zip(columns, row, strict=strict)))
return result
if __name__ == "__main__":
columns = ["name", "score"]
rows = [["ann", 95], ["bob", 88]]
for item in rows_to_dicts(columns, rows):
print(item){'name': 'ann', 'score': 95}
{'name': 'bob', 'score': 88}这里strict=True是双保险:函数里已经用len检查过一次,zip的strict负责在真正拼接时再兜一次底。
五、zip 与其它工具的关系
| 工具 | 产出 | 长度不等时 | 备注 |
|---|
zip(a, b) | 迭代器,产出元组 | 截断到最短 | Python 3 起是迭代器 |
zip(a, b, strict=True) | 同上 | 抛ValueError | 需 3.10+ |
itertools.zip_longest(a, b) | 同上 | 用fillvalue补齐 | 结果最长 |
enumerate(x) | 迭代器,产出(序号, 元素) | 不涉及 | 只有一个序列 |
map(f, a, b) | 迭代器 | 截断到最短 | 直接对每对元素调用f |
map其实也能接受多个可迭代对象,效果类似zip加处理函数,它同样在最短处停止。
常见坑点
- 把 zip 对象当成可重复遍历的容器
❌z = zip(a, b),第一次for用完,第二次for什么都不输出 ✅ 迭代器一次性;要多次使用就重新zip(a, b)或先list()
- 以为长度不等会报错
❌for k, v in zip(keys, values),少了几个值却毫无提示 ✅ 确信等长就加strict=True(3.10+),或先自查长度
- 在 zip 里传进「一次性」的迭代器后重复使用
❌ 传的是生成器,zip消费后再用同一个生成器得到空 ✅ 生成器同样只能消费一次,需要复用就先物化成list
- 以为
zip(*matrix)返回列表
❌zip(*matrix).append(...),AttributeError✅ 它是迭代器,要list(zip(*matrix))才能当下标访问
- 解包后忘了得到的是元组
❌nums, letters = zip(*pairs)之后对nums调append✅ 解包结果是元组,需要列表就再list(...)
- 误用 Python 2 的预期
❌ 照抄老代码里「zip返回列表、可以len(zip(...))」的用法 ✅ Python 3 里zip返回迭代器,没有len;要长度先转list
strict参数用在不支持的版本
❌ 在 3.9 及以前写zip(a, b, strict=True)✅strict是3.10 新增(PEP 618);旧版本自行比较长度
- 用 zip 传单个长序列却忘记它是单元素元组
❌for x in zip(data): ...之后把x当元素用 ✅ 单参数zip产出的是(元素,)这样的一元组
总结
| 关注点 | 结论 |
|---|
| 签名 | zip(*iterables, strict=False) |
| 返回类型 | 迭代器,产出元组 |
| 惰性 | 是,迭代时才取值 |
| 可变现次数 | 一次 |
| 长度不等默认行为 | 截断到最短 |
strict=True | 3.10+,不等长抛ValueError |
| 典型搭档 | dict()、enumerate、解包、itertools.zip_longest |
zip的用法可以用一句话概括:它负责把多个序列按位置对齐。用的时候盯紧三件事——它是迭代器、它只能消费一次、它默认按最短截断且不报警。第三点尤其值得警惕,因为被丢掉的元素不会以任何形式提醒你。