zip:长度对不齐别再默默截断了
Python 3.10+ 的 zip(..., strict=True):默认 zip 按最短截断会静默丢数据;假定等长时用 strict=True 直接 ValueError,缺列缺字段当场暴露。
要解决的问题
写 Python 后端时,经常要把两条(或多条)序列「并排」处理:字段名和字段值、用户 ID 和权限位、CSV 两列、批量接口里的 keys 与 payloads。第一反应往往是 for a, b in zip(xs, ys)。
默认的 zip 有个安静的特性:一旦最短的那条耗尽就停,更长序列里多出来的元素会被直接丢掉。若你本来以为两边一样长——比如 JSON 反序列化丢了一项、DB 查询少了一行、手工拼列表时漏了尾部——程序不会报错,只会悄悄少处理几条。后面的断言、落库、对账才偶发失败,排查成本很高。
自 Python 3.10 起,zip 多了关键字参数 strict=True:长度一致时行为和以前一样;一旦某条先耗尽、另一条还有剩余,立刻抛出 ValueError。本文示例已在本机 Python 3.13.5 跑通。
核心概念
以 Python 3.13 内置函数文档 · zip 为准:
zip(*iterables, strict=False):并行迭代多条可迭代对象,产出元组;第 i 个元组来自各参数的第 i 个元素。它是惰性的——包进list()、放进for,或传给dict()之类才会真正消费。- 默认(
strict=False):停在最短的那条;更长序列的尾部被忽略。 strict=True(3.10+):假定各参数应等长。输出与默认相同;若某条先耗尽而其他还有元素,抛ValueError(文档示例文案形如zip() argument 2 is longer than argument 1)。- 三种长度策略(文档原意):默认截断;
strict=True要求等长并报错;需要「补齐到最长」时用itertools.zip_longest(可设fillvalue)。 - 边界:只有一个参数时,得到一元组序列;无参数时得到空迭代器。也可用
zip(*zip(x, y))做「解压」还原列。 - 分块 idiom:
zip(*[iter(s)] * n, strict=True)把序列切成固定长度 n 的块;长度不能整除时会ValueError(文档推荐在「必须完整成组」时加strict=True)。
对应变更见 What's New In Python 3.10 与 PEP 618。
示例代码
先看默认截断 vs strict=True(与官方文档一致,本地已验证):
# 默认:按最短截断,多出来的 'fum' 被丢掉
assert list(zip(range(3), ["fee", "fi", "fo", "fum"])) == [
(0, "fee"),
(1, "fi"),
(2, "fo"),
]
# 等长时,strict=True 与默认结果相同
assert list(zip(("a", "b", "c"), (1, 2, 3), strict=True)) == [
("a", 1),
("b", 2),
("c", 3),
]
# 不等长 + strict=True → ValueError
try:
list(zip(range(3), ["fee", "fi", "fo", "fum"], strict=True))
except ValueError as e:
assert "longer than" in str(e)
后端里更常见的是「表头 / 行值必须对齐」或「两列配置必须成对」:
keys = ["id", "name", "email"]
vals = [101, "Ada", "ada@example.com"]
row = dict(zip(keys, vals, strict=True))
assert row == {"id": 101, "name": "Ada", "email": "ada@example.com"}
# 少了一个值:立刻失败,而不是悄悄丢掉 email 键
try:
dict(zip(keys, [101, "Ada"], strict=True))
except ValueError as e:
assert "shorter than" in str(e)
若业务上就是「允许缺省、用占位符补齐」,别硬套 strict,改用 zip_longest:
from itertools import zip_longest
assert list(
zip_longest(range(3), ["fee", "fi", "fo", "fum"], fillvalue="?")
) == [
(0, "fee"),
(1, "fi"),
(2, "fo"),
("?", "fum"),
]
需要把字节流 / 字符序列切成固定宽度块、且长度必须能整除时,可用文档里的分块写法:
s = "ABCDEF"
assert list(zip(*[iter(s)] * 3, strict=True)) == [
("A", "B", "C"),
("D", "E", "F"),
]
try:
list(zip(*[iter("ABCDE")] * 3, strict=True))
except ValueError as e:
assert "shorter than" in str(e)
解压(unzip)也仍然成立:
x = [1, 2, 3]
y = [4, 5, 6]
assert list(zip(x, y)) == [(1, 4), (2, 5), (3, 6)]
x2, y2 = zip(*zip(x, y))
assert x == list(x2) and y == list(y2)
常见误区
-
以为
zip「对不齐就会报错」
不会。默认是截断。只有你显式写了strict=True(且运行在 3.10+)才会因长度不一致失败。 -
在 3.9 及更早写
strict=True
strict是 3.10 才加入的关键字参数。旧版本会TypeError。部署镜像、CI、线上解释器版本要先确认;兼容旧环境时只能先自己len校验,或统一升级。 -
该截断 / 该补齐时也盲目加
strict=True
文档写得很清楚:有时长度不同是有意设计。例如只取前 N 对、或用占位符填到最长——前者用默认zip,后者用zip_longest。strict适合「契约上必须等长」的场景。 -
只在构造 list 时检查,却忘了惰性
zip(..., strict=True)在迭代过程中发现长度不一致才抛错。写成z = zip(a, b, strict=True)本身不会立刻失败;要等到for/list(z)/dict(z)消费时才会炸。测试要覆盖完整消费路径。 -
用
len(a) == len(b)代替strict,却碰上无限 / 惰性迭代器
对 list/tuple 先比长度可以,但对生成器、文件行迭代器往往不能(或不该)先len。strict=True在流式消费时仍能在「提前耗尽」的那一刻报错,更贴合惰性语义。
小结
并行配对序列时,先问自己一句:两边本该一样长吗? 本该一样长 → Python 3.10+ 用 zip(..., strict=True),把长度 bug 挡在配对现场;故意只要最短 → 默认 zip;要补齐到最长 → zip_longest。别让默认截断把缺行、缺列、缺字段变成「静默少处理」。正式项目以你当前解释器版本的 zip 文档 为准。
参考资料
谢谢你读到这里。