← 回到手记
代码

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)

常见误区

  1. 以为 zip 「对不齐就会报错」
    不会。默认是截断。只有你显式写了 strict=True(且运行在 3.10+)才会因长度不一致失败。

  2. 在 3.9 及更早写 strict=True
    strict 是 3.10 才加入的关键字参数。旧版本会 TypeError。部署镜像、CI、线上解释器版本要先确认;兼容旧环境时只能先自己 len 校验,或统一升级。

  3. 该截断 / 该补齐时也盲目加 strict=True
    文档写得很清楚:有时长度不同是有意设计。例如只取前 N 对、或用占位符填到最长——前者用默认 zip,后者用 zip_longest。strict 适合「契约上必须等长」的场景。

  4. 只在构造 list 时检查,却忘了惰性
    zip(..., strict=True) 在迭代过程中发现长度不一致才抛错。写成 z = zip(a, b, strict=True) 本身不会立刻失败;要等到 for / list(z) / dict(z) 消费时才会炸。测试要覆盖完整消费路径。

  5. 用 len(a) == len(b) 代替 strict,却碰上无限 / 惰性迭代器
    对 list/tuple 先比长度可以,但对生成器、文件行迭代器往往不能(或不该)先 len。strict=True 在流式消费时仍能在「提前耗尽」的那一刻报错,更贴合惰性语义。

小结

并行配对序列时,先问自己一句:两边本该一样长吗? 本该一样长 → Python 3.10+ 用 zip(..., strict=True),把长度 bug 挡在配对现场;故意只要最短 → 默认 zip;要补齐到最长 → zip_longest。别让默认截断把缺行、缺列、缺字段变成「静默少处理」。正式项目以你当前解释器版本的 zip 文档 为准。

参考资料

  1. zip — Built-in Functions — Python 3.13 文档
  2. zip — 最新稳定版文档
  3. itertools.zip_longest — Python 3.13 文档
  4. What's New In Python 3.10
  5. PEP 618 – Add Optional Length-Checking To zip
✳

谢谢你读到这里。