Kodokon kodokon.com

迭代器与生成器:yield 与流水线

掌握迭代协议与生成器,以恒定的内存惰性处理数据流。

10 分钟 · 3 题

在 Kodokon 中打开本课

Python 的 for 不过是一份协议:iter(obj) 索取一个迭代器,随后不断调用 next(),直到抛出 StopIteration 异常。务必区分可迭代对象(比如列表,每次都能交出一个全新的迭代器)和迭代器(那个游标,只能被遍历一次)。理解了这份协议,也就理解了为什么某些对象会 "被消耗掉"。

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIteration
手工还原一个 for 循环所做的事

包含 yield 的函数在被调用时并不会执行:它返回一个生成器,也就是一个函数体按需运行的迭代器。每次 next() 都会推进到下一个 yield,执行在那里连同全部局部状态一起被挂起。直接的后果是:无论数据流有多大,内存占用都保持恒定 - 你是逐个产出值,而不是一次性把整个列表实体化出来。

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield 让函数在两个值之间挂起

在生产中最主要的收益是:流水线。每一级消费上一级的数据流,并惰性地产出自己的数据流;在最终的消费动作(list()sum()、一个 for)之前,什么都不会运行。有一个需要记住的取舍:列表可以被多次遍历、可以切片,生成器则不行;但另一方面,生成器能处理一个 10 GB 的文件而不会撑爆内存。生成器表达式 - (f(x) for x in xs if cond) - 无需定义函数就能覆盖那些简单的处理级。

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
一条惰性的流水线:在 list() 之前什么都不会运行

知识检测

确认你已牢记本课的重点内容。

  1. 当你调用一个函数体中包含 yield 的函数时,你得到的是什么?
    • 第一个 yield 的结果
    • 一个生成器,函数体完全没有执行
    • 一个包含所有产出值的列表
  2. 相比中间列表,生成器流水线有什么决定性的优势?
    • 结果会被自动排序
    • 内存占用保持恒定:值是按需产出的
    • 处理会被并行分配到多个核心上
  3. 在第一次 list(gen) 之后,对同一个生成器再执行一次 list(gen) 会返回什么?
    • 和第一次调用相同的列表
    • 一个空列表
    • 会向外抛出一个异常