for x in y背后只有三个约定。生成器把它们包装成语法糖,协程又在生成器上长出来——三者是同一条演化链。
你将学到
- 可迭代对象与迭代器的区别,以及
for循环到底做了什么 - 生成器函数、
yield、send/throw/close、yield from - 生成器表达式与惰性求值
- 生成器如何一步步演化成协程(
asyncio的前夜) - 用生成器处理大文件与无限序列
前置知识
04 - 上下文管理器与 with。本篇是大师篇的收尾,下一篇进入并发世界。
一、可迭代对象 vs 迭代器
两句话分清:
- 可迭代对象(iterable):实现了
__iter__,能产出迭代器; - 迭代器(iterator):实现了
__next__,能一次吐一个值,并在耗尽后抛StopIteration。
nums = [1, 2, 3]
print(hasattr(nums, "__iter__")) # 输出: True —— 列表是可迭代对象
print(hasattr(nums, "__next__")) # 输出: False —— 但列表不是迭代器
it = iter(nums) # 用 iter() 取出迭代器
print(next(it), next(it)) # 输出: 1 2
print(iter(it) is it) # 输出: True —— 迭代器的 __iter__ 返回自己
分清这一点很实用:迭代器只能走一遍。列表、元组、字典、文件对象、生成器都可迭代,但只有迭代器是「一次性」的。
二、迭代器协议:__iter__ / __next__ / StopIteration
for 循环的展开就是「先 iter(),再反复 next(),捕获 StopIteration」:
class Countdown:
def __init__(self, start): self.n = start
def __iter__(self):
return self # 自己就是迭代器
def __next__(self):
if self.n <= 0:
raise StopIteration # 用异常表示「没有了」
self.n -= 1
return self.n + 1
for x in Countdown(3):
print(x)
# 输出: 3 / 2 / 1
等价的 while 写法(这就是 for 做的事):
it = iter(Countdown(3))
while True:
try:
print(next(it)) # for 循环内部就是这么干的
except StopIteration: # 异常被 for 悄悄接住
break
自己写迭代器类很啰嗦——这正是生成器要解决的痛点。
三、生成器函数与 yield
函数体里出现 yield,它就不再是普通函数,而是生成器函数:调用它只会得到生成器对象,函数体一行都不执行:
def countdown(n):
print("开始")
while n > 0:
yield n # 产出一个值并暂停在这里
n -= 1
print("结束")
g = countdown(3) # g 是 <generator object ...>,函数体一行未执行
print("还没开始") # 输出: 还没开始
print(next(g)) # 输出: 开始 / 3
print(list(g)) # 输出: 先打印「结束」,再打印 [2, 1]
关键性质:惰性(没有 next() 就不计算)、状态自动保存(局部变量冻结在栈帧里)、它就是迭代器(自带 __iter__ / __next__,能直接进 for)。
四、send / throw / close
yield 是个「双向通道」:它既能把值送出去,也能接收值进来。
def echo():
while True:
x = yield # yield 表达式的结果 = send 进来的值
print(f"收到 {x}")
g = echo()
next(g) # 先启动,跑到第一个 yield 处挂起
g.send("hello") # 输出: 收到 hello
g.send(42) # 输出: 收到 42
g.close() # 通知生成器结束
yield 还能出现在赋值右边,用来「产出上一个结果、接收下一个输入」:
def acc():
total = 0
while True:
x = yield total # 先返回 total,再等下一个输入
if x is None:
break
total += x
g = acc()
print(next(g)) # 输出: 0
print(g.send(10)) # 输出: 10
print(g.send(5)) # 输出: 15
throw 把异常「扔进」生成器(在挂起的 yield 处抛出),close 则在生成器内部触发 GeneratorExit:
def worker():
while True:
try:
yield # 挂起在这里等异常
except ValueError as e:
print(f"内部捕获: {e}") # 接住后继续循环,生成器不结束
g = worker()
next(g)
g.throw(ValueError("糟糕")) # 输出: 内部捕获: 糟糕
g.close()
send / throw 是 PEP 342 引入的——它们把生成器从「数据生产者」升级成「可双向对话的协程」,这是后半段的伏笔。
五、yield from:把迭代委托出去
想从一个生成器里「转发」另一个可迭代对象,yield from 比手写 for 更短,而且完整透传 send / throw / close,还能接收子生成器的返回值:
def chain(*iterables):
for it in iterables:
yield from it # 等价于 for x in it: yield x,但更强
print(list(chain([1, 2], "ab", (3, 4))))
# 输出: [1, 2, 'a', 'b', 3, 4]
委托的「返回值」会直接成为 yield from 表达式的值:
def inner():
total = 0
while True:
x = yield
if x is None:
return total # 用 return 把结果交回给委托方
total += x
def outer():
result = yield from inner() # 拿到 inner 的返回值
print("inner 返回", result)
yield "结束"
g = outer()
next(g)
g.send(1)
g.send(2)
print(g.send(None)) # 输出: inner 返回 3,然后 结束
没有 yield from 的时代,这类「协程嵌套」得手写一堆 send/throw 转发代码。asyncio 的早期实现就是靠它驱动的。
六、生成器表达式
把列表推导式的方括号换成圆括号,得到的就是生成器——惰性、不占内存:
gen = (x * x for x in range(5))
print(gen) # 输出: <generator object <genexpr> at 0x...>
print(sum(gen)) # 输出: 30
print(sum(x * x for x in range(5))) # 输出: 30(函数唯一参数时可省掉一层括号)
[x*x for x in range(10**7)] 会立刻造出上千万元素的列表;(...) 版本的内存占用几乎只和「当前这一个元素」有关。
七、生成器如何演化为协程(asyncio 前夜)
把生成器当协程用,只需要一个「调度器」轮流驱动它们:
def coro_a():
print("A: 开始")
yield # 让出控制权
print("A: 恢复")
def coro_b():
print("B: 开始")
yield
print("B: 恢复")
tasks = [coro_a(), coro_b()]
while tasks: # 一个最朴素的「事件循环」
task = tasks.pop(0)
try:
next(task)
tasks.append(task) # 没结束就放回队列尾部
except StopIteration:
pass # 结束的就不再入队
# 输出: A: 开始 / B: 开始 / A: 恢复 / B: 恢复
这段 20 行不到的东西,就是 asyncio 调度器的骨架。真实的 asyncio 在此之上加了三样东西:事件循环(loop)、Future/Task 封装、以及基于 select 的 IO 多路复用。演化链条是:
生成器函数 (yield) → 可双向通信 (PEP 342) → 可委托嵌套 (PEP 380) → 原生协程 (PEP 492)
到 PEP 492 之后,协程和生成器在语言层面彻底分家:
import asyncio
async def native():
return 42
c = native()
print(type(c)) # 输出: <class 'coroutine'>
# next(c) # TypeError: 'coroutine' object is not an iterator
c.close() # 不这样处理会报 "coroutine was never awaited" 警告
print(asyncio.run(native())) # 输出: 42
原生协程不能再 next(),必须由事件循环驱动。细节留给 06 - GIL 真相与并发模型选择 与后面的 asyncio 专篇。
八、实战:大文件与无限序列
按块读大文件——无论文件多大,内存占用都是常量:
def read_in_chunks(path, size=8192):
with open(path, "rb") as f:
while chunk := f.read(size): # 海象运算符,3.8+
yield chunk # 出一块,读一块
无限序列也能安全使用,因为有「惰性」兜底:
from itertools import islice
def fib():
a, b = 0, 1
while True: # 不设终点,靠调用方控制取多少
yield a
a, b = b, a + b
print(list(islice(fib(), 10)))
# 输出: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
itertools 里全是生成器思路的产物:islice、chain、count、groupby、takewhile——它们都只在你消费时才计算。
常见坑
坑 1:生成器只能遍历一次。
# ❌ 第二次遍历拿到空列表
g = (x for x in range(3))
print(list(g)) # 输出: [0, 1, 2]
print(list(g)) # 输出: []
# ✅ 需要多次遍历就存成列表 / 元组;只看一次才用生成器省内存
坑 2:想用 return 给 for 循环传值。
# ❌ 循环只会打印 1,那个 2 拿不到
def g():
yield 1
return 2
for x in g():
print(x) # 只打印 1,那个 2 拿不到
# ✅ 想拿到返回值,用 `yield from` 接收(见第五节)
坑 3:以为生成器表达式会立刻执行。
# ❌ 打印发生在「消费」时,不是创建时
g = (print(x) for x in range(3))
print("还没打印") # 输出: 还没打印
list(g) # 此时才输出: 0 / 1 / 2
# ✅ 需要立刻执行就用列表推导式 [print(x) for x in range(3)]
小结
- 可迭代对象实现
__iter__,迭代器实现__next__;迭代器是一次性的,for=iter()+ 反复next()+ 接住StopIteration。 - 含
yield的函数返回生成器;调用时不执行任何代码,直到第一次next()。 send/throw/close让生成器变成双向通道;yield from负责透明委托。- 生成器表达式惰性求值,内存友好;适合大文件与无限序列。
- 演化链:
yield→send/throw→yield from→async/await。asyncio的调度器就诞生在这条链的尽头。
延伸阅读
- Python 官方文档 Iterators 与 itertools
- 《Fluent Python》第 17 章「Generators」;PEP 342 / 380 / 492 连着读,演化脉络一目了然
文章回复
0 条公开回复