Python zip 严格配对:长度不一致何时才报错,已经做的操作怎么办

10-01 4阅读

两列数据配对之前,先决定能否少一行

导入清单时,一列是物品编号,另一列是对应数量。如果编号有三项,数量只有两项,默认的 zip 会在较短输入结束时停止,第三个编号不会出现在结果里。程序正常结束,却少处理一项,这种错误往往比直接报错更难发现。需要逐项对应的数据,应把等长要求明确写进程序。否则下游看到的只是两条看似完整的记录,很难知道上游原本还有第三个编号。

Python 从三点十版本开始支持 strict=True。这个参数让长度不一致变成 ValueError,但不会先扫描两份输入,也不会在创建 zip 对象时就完成校验。它仍是惰性迭代器,只有实际向它索取下一组数据,检查才会继续向前推进。

观察错误之前已经发生的事

下面保存为 Python 文件,用三点十及以上版本运行。示例只修改内存里的列表。先创建配对器,再取第一项,最后继续遍历到末尾;这样能把创建成功、部分成功和最终失败三个时刻分开。这里故意保留已经追加的记录,以便检查异常是否替我们恢复了状态。

Python zip 严格配对:长度不一致何时才报错,已经做的操作怎么办

AI生成概念示意图,非真实界面

ids = iter(['A', 'B', 'C'])
amounts = iter([4, 9])
pairs = zip(ids, amounts, strict=True)
written = []
print('created:', len(written))
first = next(pairs)
written.append(first)
print('first:', first)
try:
    for pair in pairs:
        written.append(pair)
except ValueError:
    print('mismatch: detected')
else:
    raise AssertionError('mismatch was missed')
assert written == [('A', 4), ('B', 9)]
print('partial:', written)
remaining = list(ids)
assert remaining == []
print('remaining ids:', remaining)

def prepare(names, counts):
    rows = list(zip(names, counts, strict=True))
    if any(type(count) is not int or count < 0 for _, count in rows):
        raise ValueError('counts must be nonnegative integers')
    return rows

published = []
try:
    draft = prepare(['A', 'B', 'C'], [4, 9])
except ValueError:
    print('staging rejected:', published)
else:
    raise AssertionError('invalid input was accepted')
assert published == []
draft = prepare(['A', 'B'], [4, 9])
published.extend(draft)
assert published == [('A', 4), ('B', 9)]
print('published:', published)

for left, right, should_pass in [([1], [2], True), ([], [], True),
                                 ([1], [2, 3], False),
                                 ([1, 2], [3], False)]:
    try:
        list(zip(left, right, strict=True))
    except ValueError:
        assert not should_pass
    else:
        assert should_pass
for invalid in [True, -1, '4']:
    try:
        prepare(['A'], [invalid])
    except ValueError:
        pass
    else:
        raise AssertionError('invalid count was accepted')
print('boundary checks passed')

created 输出零,first 显示 A 与四。继续遍历时,B 与九也会被加入 written,然后才出现 mismatch。partial 仍然包含两条记录,说明捕获异常没有撤销之前的追加。remaining ids 是空列表:检测第三组时,编号 C 已经从迭代器中取出,尽管它没能组成一个完整结果。

严格检查必须消费到结束

如果循环中途 break,或调用方只拿第一项就停止,就不能据此宣布输入等长。错误可能藏在尚未读取的尾部。把 zip 放进 list 会消费到结束,但这也意味着必须等待全部输入,并为配对结果分配内存。无限输入或非常大的数据流不适合无条件这样处理。

不要在出错后继续复用原来的输入迭代器,假设它仍停在最后一个成功位置。迭代器通常只能向前走,长度探测还可能额外消耗元素。若需要重试,应从可重新打开的来源建立新输入,并根据业务标识确定哪些记录已经处理,避免把同一项重复写入。

先准备结果,再进入有副作用的阶段

prepare 先收集完整配对,再检查数量必须为非负整数,明确拒绝布尔值。长度错误时函数无法返回 draft,published 仍为空;成功返回后才追加两条记录。这里验证的是准备阶段失败不会触碰目标列表,不能把它扩大成任意业务操作都具有原子性的结论。

即使先转成列表,读取来源本身也可能有副作用,例如消费队列或推进带状态的生成器。这个写法只推迟后面的处理动作,不会倒带输入。若只是两个稳定的列表,可以先比较长度;面对一般迭代器,不能假定存在 len,也不能为了统计长度先把输入耗尽。

需要边读边写数据库的大批量导入,可以把整批写入放入明确的事务,并让长度异常触发回滚。已经发出的消息、网络请求或其他系统里的修改,不会因为 Python 抛出异常就自动撤销;这类流程需要另行设计提交边界、幂等处理或补偿动作。

最后四组断言覆盖等长非空、同时为空、左边较短和右边较短。异常只核对类型,不依赖具体报错文字。还应人工检查部分结果列表确实留下了两项,防止测试只关注“抛错了”,却漏掉错误发生之前已经改变的业务状态。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。