7.1 迭代器与生成器


7.1 迭代器与生成器

本节摘要:生成器函数与普通函数在语法上只差一个 yield,在执行模型上却差一个维度——它的栈帧可以被冻结与恢复。本节先补全迭代协议的类实现版,再建立"冻结帧"模型,用它解释惰性求值的内存红利、yield from 的委托语义,以及生成器表达式的适用面。

手写迭代器:协议的类实现

3.1 节已经用 iter/next 复现过 for,现在从类的角度把协议补全——一个可迭代对象 + 一个迭代器:

class Countdown: def __init__(self, n): self.n = n def __iter__(self): # 可迭代协议:把自己交给 for return CountdownIter(self.n) class CountdownIter: def __init__(self, n): self.cur = n def __iter__(self): # 迭代器通常也可迭代(返回自己) return self def __next__(self): if self.cur <= 0: raise StopIteration # 停止信号 self.cur -= 1 return self.cur + 1 for x in Countdown(3): print(x, end=" ") # 3 2 1

要维护的状态(cur)住在迭代器对象里。但一个"只为一趟循环建两个类"的写法未免隆重——于是生成器登场,把同样的状态机压缩成函数形态。

yield:撕开"一口气跑完"的规则

普通函数的帧是"点火即烧完":进入、执行、返回、帧销毁。生成器函数的帧可以中途熄火、保存现场、择日续烧

>>> def countdown(n): ... print("启动") ... while n > 0: ... yield n # 产出值并冻结 ... n -= 1 ... print("结束") >>> g = countdown(3) # 注意:什么都没打印!帧未启动 >>> next(g) # 启动,跑到 yield 冻结 启动 3 >>> next(g) # 从冻结点续跑 2 >>> next(g); next(g) 1 结束 StopIteration

g = countdown(3) 不执行函数体,只造出生成器对象——这是第一个认知关口。next 驱动帧跑到下一个 yield 并冻结,局部变量、循环位置、执行指针全部保留。用 dis 可以看到关停点就是字节码指令流里的 YIELD_VALUE。

生成器帧的冻结与恢复

生成器帧的冻结与恢复

惰性流水线:内存红利的实战

生成器最大的实战价值是把中间结果从内存里赶出去。对比同一任务的两个版本——统计一个大日志里含 ERROR 的行的长度:

# 物化版:三层列表,每层都是完整的大列表 lines = open("app.log", encoding="utf-8").readlines() errors = [ln for ln in lines if "ERROR" in ln] lengths = [len(ln) for ln in errors] # 惰性版:同构的生成器管道,任意一层都不占成块内存 def error_lines(path): with open(path, encoding="utf-8") as f: for ln in f: if "ERROR" in ln: yield ln def lengths_of(seq): for x in seq: yield len(x) total = sum(lengths_of(error_lines("app.log")))

管道里每个元素"出生即消费、消费即死亡",内存占用是 O(1) 级别而不是 O(n)。生成器表达式是这类管道的语法糖(圆括号版推导式):

total = sum(len(ln) for ln in open("app.log", encoding="utf-8") if "ERROR" in ln)

作为函数唯一实参时连外层括号都可省。取舍要清楚:惰性管道只能消费一次(3.1 节的坑)、每个元素多一层 next 开销——数据要反复用就先物化成 list。

yield from:委托与双向通道

yield from iterable 把"逐个转产"压缩成一句,还悄悄建立了与子生成器的双向通信:

def flatten(nested): for item in nested: if isinstance(item, list): yield from flatten(item) # 委托给递归生成器 else: yield item list(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]

没有 yield from 的等价写法是 for x in flatten(item): yield x——但前者还额外打通了 send/throw 通道(外层可以直接与最内层生成器通信),这正是早期协程的实现基础。async/await 的语法演进路径(yield → yield from → await)在这里能找到完整伏笔,第 8 章会接上。

顺带认识两个 itertools 常客,它们都是生成器思维的产物:

>>> from itertools import islice, count, chain >>> list(islice(count(10, 5), 3)) # 无限计数器,切前 3 个 [10, 15, 20] >>> list(chain("ab", "cd")) # 惰性拼接 ['a', 'b', 'c', 'd']

⚠️ 常见坑:生成器只能消费一次,第二次迭代为空;在生成器里 return 值会附在 StopIteration 上(极少需要读它);生成器表达式传给需要多次迭代的 API(如 sorted 两次遍历)会得到第二次为空。

💡 关键直觉:看到大列表的链式推导,就问一句"中间列表有人用吗"——没有就降级成生成器管道。数据流思维替代数据块思维,是中级到高级的一道坎。

深挖:生成器的 send 与双向通信

生成器不只能吐值,还能被喂值——send 方法让数据反向流入冻结点。机制:yield 表达式有返回值,received = yield produced 在冻结时产出 produced,被 send 唤醒后 received 拿到传入的值并继续执行。首次唤醒必须用 send(None) 或 next(还没冻结到任何 yield 点,无处接收值)。这套双向通道在早期是手写协程的全部基础设施——用 send 投递任务、用 yield 挂起等待;async/await 落地后手写协程退居幕后,但机制没有消失,理解 send 就理解了协程"可暂停、可唤醒、可传值"三件套的来源。另一个冷门但关键时刻救命的细节:生成器被垃圾回收或被 close 时,会在冻结点抛 GeneratorExit 异常,生成器内部可以用 try/finally 保证清理逻辑(关闭文件、释放连接)执行——用生成器包装资源的写法,正确性正建立在这条保证上。

本节要点回顾

  • 协议类实现__iter__ 发迭代器、__next__ 供值、StopIteration 收尾——生成器是它的函数形态糖衣。
  • 冻结帧模型:调用不执行、next 跑到 yield 冻结、局部状态与指令指针全保留。
  • 惰性管道:元素级流转 O(1) 内存;代价是一次性消费与轻微开销。
  • yield from:委托 + 双向通道,递归扁平化与协程雏形。
  • 生成器表达式:单实参时最省字符;需复用先物化。
  • itertools:count/islice/chain 是惰性思维的现成零件。

下一节的主角装饰器,本质是"把'用新 callable 换旧名字'这个动作"做成了语法——一等函数知识的集大成应用。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U