Skip to content

迭代器与生成器

Iterable 与 Iterator

  • Iterable 能通过 iter(obj) 产生 Iterator。
  • Iterator 通过 next(iterator) 逐项返回数据,结束时抛出 StopIteration
  • Iterator 通常也是 Iterable,且 iter(iterator) is iterator
python
values = [1, 2, 3]
iterator = iter(values)
next(iterator)  # 1
next(iterator)  # 2

List 可重复迭代;Iterator 消费后不会自动重置。

生成器

yield 的函数调用时返回 Generator,函数体直到第一次 next 才执行:

python
def read_ids(rows):
    for row in rows:
        yield row["id"]
python
ids = (row["id"] for row in rows)   # Generator Expression
ids = [row["id"] for row in rows]   # 立即创建 List

生成器降低峰值内存,但把错误和 IO 推迟到消费阶段。返回生成器的函数如果离开 with open(...) 后才被迭代,文件可能已经关闭;资源生命周期必须覆盖消费过程。

耗尽与隐式消费

以下操作都会消费 Iterator:

python
list(iterator)
sum(iterator)
any(iterator)
all(iterator)
x in iterator

anyall 会短路,只消费到能够确定结果的位置。调试时先 list(iterator) 会让正式逻辑看到空数据,是常见问题。

需要同时遍历可用 itertools.tee,但它会缓存两条迭代速度之间的差值;差距很大时可能占用大量内存。

zip 的截断

python
list(zip([1, 2], ["a"]))  # [(1, "a")]

默认 zip 在最短 Iterable 结束时停止,可能静默丢弃较长输入的尾部。长度必须一致时使用:

python
zip(left, right, strict=True)

enumerate

python
for index, value in enumerate(values, start=1):
    ...

不要使用 range(len(values)) 仅为了同时获取索引和值。需要并行修改原 List 时,仍要明确赋值目标与迭代期间结构变化的风险。

迭代时修改容器

遍历 List 时删除元素会移动索引,从而跳过内容:

python
values[:] = [x for x in values if keep(x)]

遍历 Dict 或 Set 时改变大小会抛出 RuntimeError。需要修改时遍历快照:

python
for key in list(mapping):
    if should_remove(key):
        del mapping[key]

yield from

yield from iterable 不只是 for 循环缩写,还会在生成器协作中传递 sendthrowclose 和子生成器返回值。普通扁平化可以简单理解为委托迭代;实现协程协议时需要了解完整语义。