Python内置zip函数迭代器消费顺序行为的文档歧义问询
官方文档现有说明
Python文档中关于zip()函数有如下警告:
需要注意的是,传递给zip()的可迭代对象长度可能不同;有时是有意为之,有时则是准备这些可迭代对象的代码存在bug。Python提供三种不同的处理方式:
默认情况下,zip()会在最短的可迭代对象耗尽时停止。它会忽略较长可迭代对象中的剩余项,将结果截断为最短可迭代对象的长度:
>>>
>>> list(zip(range(3), ['fee', 'fi', 'fo', 'fum']))
[(0, 'fee'), (1, 'fi'), (2, 'fo')]
文档还建议使用strict参数,在传入的可迭代对象长度不等时抛出异常,并且末尾有明确警告:
如果不使用strict=True参数,任何导致可迭代对象长度不同的bug都会被隐藏,可能在程序其他部分表现为难以发现的bug。
迭代器消费顺序依赖问题
实际使用中发现,zip()对可迭代对象的消费行为依赖于传入参数的顺序:若较长的可迭代对象先传入,可能会意外丢失元素。
示例代码:
r = range(3) it = iter(r) list(zip('12', it)) print(f'list(it) => {list(it)} <= happy case, this is expected') it = iter(r) list(zip(it, '12')) print(f'list(it) => {list(it)} <= sad case, an element is consumed from the iterator')
输出结果:
list(it) => [2] <= happy case, this is expected list(it) => [] <= sad case, an element is consumed from the iterator
行为原因分析
从CPython源码的zip_next函数逻辑可以解释该行为:循环会依次从每个传入参数中消费一个元素,若较短的可迭代对象已耗尽,已消费的元素会被丢弃。
for (i=0 ; i < tuplesize ; i++) { it = PyTuple_GET_ITEM(lz->ittuple, i); item = (*Py_TYPE(it)->tp_iternext)(it); if (item == NULL) { Py_DECREF(result); if (lz->strict) { goto check; } return NULL; } olditem = PyTuple_GET_ITEM(result, i); PyTuple_SET_ITEM(result, i, item); Py_DECREF(olditem); }
对文档描述的异议
文档中描述:
它会忽略较长可迭代对象中的剩余项,将结果截断为最短可迭代对象的长度
该描述并不准确,因为zip()并没有预看可迭代对象长度的机制,实际是按顺序消费元素,当某一可迭代对象耗尽时,已从后续可迭代对象消费的元素会被丢弃,导致较长可迭代对象的元素丢失情况和传入顺序相关。
问题归类
这应归类为选项3:都不是,已有警告,但文档最好补充该情况。
- 不是行为bug:该行为是CPython实现的既定逻辑,符合迭代器的消费特性;
- 不是文档bug:现有文档的描述是从结果长度角度的简化说明,并非完全错误;
- 补充该场景的说明能帮助开发者更全面理解
zip()的行为,避免潜在的隐蔽问题。
内容的提问来源于stack exchange,提问作者Gregory Kuhn

