Python 生成器的 send、throw、close 方法是干什么的?
简化版
**普通生成器只能「产出值」(用 next() 或 for 遍历),但生成器其实是「双向」的——通过 send()、throw()、close() 三个方法,调用方可以「往生成器里传值/抛异常/关闭它」,让生成器变成一个能「接收」也能「产出」的协程。**三个方法:① send(value)——恢复生成器执行,并把 value 作为「当前暂停的 yield 表达式」的返回值传进去(x = yield 里 x 收到 value),同时返回下一个 yield 的值;next(gen) 等价于 gen.send(None);② throw(exc)——在生成器当前暂停处「抛入」一个异常,让生成器内部能 try/except 处理(或传播出来);③ close()——在生成器暂停处抛入 GeneratorExit,请求生成器结束(可用于清理资源,finally 块会执行)。关键点:第一次必须先 next()(或 send(None))「预激」生成器到第一个 yield 暂停,才能 send 有意义的值。用途:协程(累加器、状态机)、可暂停/恢复的任务、资源清理。核心记忆:send 传值进生成器(yield 表达式的返回值)+ 恢复执行、throw 抛异常进去、close 请求结束;第一次要先 next 预激;这让生成器成为双向协程。
详细版
三个方法:
| 方法 | 作用 |
|---|---|
gen.send(v) | 传值进生成器(成为 yield 表达式的值)+ 恢复执行,返回下个 yield |
gen.throw(exc) | 在生成器暂停处抛入异常 |
gen.close() | 抛入 GeneratorExit,请求结束(清理资源) |
next(gen) | 等价 gen.send(None) |
# send:往生成器里传值
def echo():
while True:
received = yield # yield 表达式接收 send 的值
print(f"收到: {received}")
g = echo()
next(g) # 预激:执行到第一个 yield 暂停(必须!)
g.send("hello") # 收到: hello("hello" 成为 received)
g.send("world") # 收到: world
# send 同时接收和产出
def accumulator():
total = 0
while True:
x = yield total # 产出 total,同时 x 接收 send 的值
total += x
acc = accumulator()
next(acc) # 预激,产出初始 total=0
print(acc.send(10)) # 传入 10,total=10,产出 10
print(acc.send(5)) # 传入 5,total=15,产出 15
# throw:往生成器里抛异常
def worker():
try:
while True:
x = yield
print(f"处理 {x}")
except ValueError:
print("收到 ValueError,清理中")
yield "cleaned"
w = worker(); next(w)
w.send(1) # 处理 1
w.throw(ValueError) # 收到 ValueError,清理中
# close:请求生成器结束
def resource():
print("打开资源")
try:
while True:
yield
finally:
print("关闭资源") # close 时 finally 执行
r = resource(); next(r) # 打开资源
r.close() # 关闭资源(GeneratorExit 触发 finally)
# 第一次不预激就 send 值会报错
g2 = echo()
# g2.send("x") # ✗ TypeError: can't send non-None value to a just-started generator
⚠️ 核心认知:
yield不只是「产出值的出口」,它同时是「接收值的入口」——x = yield value这个表达式,value是产出给调用方的,而x会在下次send(v)时收到v;正是这个「双向」特性让生成器能当协程用。理解的关键是「暂停点」:生成器执行到yield就暂停、把控制权交回调用方;调用方用next()/send()恢复它——next()恢复且让 yield 表达式返回None,send(v)恢复且让 yield 表达式返回v。三个方法的分工:send传值、throw传异常、close传「结束信号」(GeneratorExit)。有个必须记住的规矩:新创建的生成器停在「最开始」(还没到第一个 yield),必须先next()(或send(None))「预激(prime)」,把它推进到第一个 yield 暂停点,之后send(有意义的值)才有地方接收——否则send(非None)会报TypeError(因为还没有暂停的 yield 表达式来接收这个值)。这三个方法是「生成器作为协程」的基础,也是理解yield from双向转发和早期 asyncio 的前提。
完整版教学
一、yield 是双向的
先理解 yield 既产出又接收:
普通认知:yield 只是"产出值"
def gen():
yield 1 # 产出 1
yield 2 # 产出 2
→ 用 next() / for 取值
进阶:yield 是"双向"的
x = yield value
→ value:产出给调用方(out)
→ x:下次 send(v) 时收到 v(in)
yield 表达式的"值"来自 send:
next(gen) → yield 表达式返回 None
gen.send(v) → yield 表达式返回 v
执行流程(暂停点):
生成器执行到 yield 就"暂停",控制权交回调用方
调用方恢复它:
next() / send(None):恢复,yield 返回 None
send(v):恢复,yield 返回 v
→ 生成器从暂停的 yield 处继续,用返回值往下走
图示:
调用方 生成器
next(g) ─────────> 执行到 yield value 暂停
<───────── 产出 value
g.send(x) ───────> yield 返回 x,继续执行
<───────── 执行到下个 yield 暂停
所以 yield 双向:x=yield value,value 产出给调用方、x 接收 send 的值;yield 是暂停点
普通认知:yield 只是「产出值」(yield 1 产出 1、用 next/for 取值)。进阶:yield 是「双向」的——x = yield value:value 产出给调用方(out)、x 下次 send(v) 时收到 v(in)。yield 表达式的「值」来自 send:next(gen) → yield 表达式返回 None、gen.send(v) → yield 表达式返回 v。执行流程(暂停点):生成器执行到 yield 就「暂停」、控制权交回调用方,调用方恢复它(next/send(None) 恢复且 yield 返回 None、send(v) 恢复且 yield 返回 v)、生成器从暂停的 yield 处继续。理解「yield 双向:x=yield value,value 产出给调用方(out)、x 接收 send 的值(in);yield 表达式的值来自 send(next 返回 None、send(v)返回 v);yield 是暂停点、调用方 next/send 恢复」,就理解了 yield 是双向的。
二、send:传值进生成器
理解 send 的作用:
gen.send(value):
① 恢复生成器执行(从当前暂停的 yield 处继续)
② 把 value 作为"当前 yield 表达式的返回值"传进去
③ 生成器执行到下一个 yield,返回那个 yield 的值
received = yield # send(v) 时 received = v
gen.send("hi") # received 收到 "hi"
next 和 send 的关系:
next(gen) 等价 gen.send(None)
→ next 就是"传 None 进去"的 send
预激(priming)——第一次必须 next 或 send(None):
新生成器停在"最开始"(还没到第一个 yield)
→ 没有暂停的 yield 表达式来接收值
→ 必须先 next() 推进到第一个 yield 暂停
→ 之后 send(有意义的值) 才有地方接收
g = gen()
g.send("x") # ✗ TypeError(刚启动,不能 send 非 None)
next(g) # ✓ 预激(推进到第一个 yield)
g.send("x") # ✓ 现在可以了
send 同时接收和产出(典型模式):
def coroutine():
result = None
while True:
x = yield result # 产出 result,接收 send 的 x
result = process(x)
自动预激装饰器(常见技巧):
def coroutine(func):
def wrapper(*args, **kw):
g = func(*args, **kw)
next(g) # 自动预激
return g
return wrapper
所以 send(v)恢复执行+把 v 传给 yield 表达式+返回下个 yield;第一次必须 next 预激
gen.send(value):① 恢复生成器执行(从当前暂停的 yield 处继续)、② 把 value 作为「当前 yield 表达式的返回值」传进去、③ 生成器执行到下一个 yield 返回那个 yield 的值(received = yield,gen.send("hi") 时 received 收到 “hi”)。next 和 send 的关系:next(gen) 等价 gen.send(None)(next 就是传 None 的 send)。预激(priming)——第一次必须 next 或 send(None):新生成器停在「最开始」(还没到第一个 yield、没有暂停的 yield 表达式接收值),必须先 next() 推进到第一个 yield 暂停、之后 send(有意义的值) 才有地方接收(g.send("x") 刚启动会 TypeError、先 next(g) 预激才行)。自动预激装饰器(常见技巧):包一层自动 next(g)。理解「send(v):恢复执行+把 v 传给当前 yield 表达式+返回下个 yield 的值;next(gen)=send(None);★第一次必须 next 预激(新生成器还没到第一个 yield、不能 send 非 None、否则 TypeError);常用自动预激装饰器」,就掌握了 send。
三、throw:抛异常进生成器
理解 throw 的作用:
gen.throw(异常):在生成器"当前暂停处"抛入一个异常
→ 就像在暂停的 yield 那一行 raise 了这个异常
→ 生成器内部可以 try/except 捕获处理
def worker():
try:
while True:
x = yield
process(x)
except SomeError:
print("捕获到,清理") # throw 抛入的异常在这处理
w.throw(SomeError) # 在 yield 处抛入 SomeError
处理结果(三种):
① 生成器 catch 并继续 yield → throw 返回下个 yield 的值
② 生成器 catch 后正常结束 → 抛 StopIteration
③ 生成器不 catch → 异常传播回调用方(throw 处抛出)
用途:
① 通知生成器"发生了错误,请处理/清理"
② 状态机里注入异常状态
③ 配合 yield from 转发异常
和普通异常的区别:
普通异常:生成器内部代码自己出错
throw:调用方"主动注入"异常到生成器暂停处
→ 一种调用方 → 生成器的"控制信号"
所以 throw(异常)在生成器暂停处抛入异常,内部可 try/except 处理,是控制信号
gen.throw(异常):在生成器「当前暂停处」抛入一个异常——就像在暂停的 yield 那一行 raise 了这个异常、生成器内部可以 try/except 捕获处理(w.throw(SomeError) 在 yield 处抛入、被内部 except SomeError 处理)。处理结果(三种):① 生成器 catch 并继续 yield → throw 返回下个 yield 的值、② 生成器 catch 后正常结束 → 抛 StopIteration、③ 生成器不 catch → 异常传播回调用方(throw 处抛出)。用途:① 通知生成器发生了错误请处理/清理、② 状态机里注入异常状态、③ 配合 yield from 转发异常。和普通异常的区别:普通异常是生成器内部代码自己出错、throw 是调用方「主动注入」异常到生成器暂停处(一种调用方→生成器的控制信号)。理解「throw(异常)在生成器暂停处抛入异常(像在 yield 那行 raise),内部可 try/except 处理;结果:catch 继续/catch 结束抛 StopIteration/不 catch 传播回调用方;是调用方注入的控制信号」,就掌握了 throw。
四、close:请求生成器结束
理解 close 和资源清理:
gen.close():请求生成器结束
→ 在生成器暂停处抛入 GeneratorExit 异常
→ 生成器应该"结束"(清理资源后退出)
行为:
① 在暂停的 yield 处抛入 GeneratorExit
② 生成器的 finally 块会执行(清理资源)
③ 生成器正常结束(GeneratorExit 被吞掉,不传播)
资源清理模式(最重要的用途):
def resource():
conn = open_connection() # 获取资源
try:
while True:
data = yield
conn.send(data)
finally:
conn.close() # close() 时 finally 执行,清理
r = resource(); next(r)
# ... 使用 ...
r.close() # 触发 finally,关闭 conn
GeneratorExit 的处理:
① 生成器不捕获 → 正常结束(推荐)
② 生成器捕获 GeneratorExit 后可清理,但:
- 不能再 yield(会 RuntimeError: generator ignored GeneratorExit)
- 应该让它结束(re-raise 或正常退出)
for 循环 / with 也会触发清理:
生成器被垃圾回收时也会调用 close(触发 finally)
→ 但显式 close 或 with 更可靠
自动清理(contextlib):
from contextlib import closing
with closing(gen()) as g:
... # 退出 with 时自动 g.close()
所以 close()抛入 GeneratorExit 请求结束,finally 块执行清理资源;不能捕获后再 yield
gen.close():请求生成器结束——在生成器暂停处抛入 GeneratorExit 异常、生成器应该「结束」(清理资源后退出)。行为:① 在暂停的 yield 处抛入 GeneratorExit、② 生成器的 finally 块会执行(清理资源)、③ 生成器正常结束(GeneratorExit 被吞掉不传播)。资源清理模式(最重要的用途):try: while True: yield finally: conn.close()——close() 时 finally 执行清理。GeneratorExit 的处理:① 生成器不捕获 → 正常结束(推荐)、② 捕获 GeneratorExit 后可清理但不能再 yield(会 RuntimeError: generator ignored GeneratorExit、应该让它结束)。for/with 也会触发清理:生成器被垃圾回收时也会调用 close(但显式 close 或 with 更可靠、contextlib.closing 自动 close)。理解「close()抛入 GeneratorExit 请求结束,finally 块执行清理资源;不捕获则正常结束(推荐)、捕获后不能再 yield(RuntimeError);垃圾回收也触发、但显式 close/contextlib.closing 更可靠」,就掌握了 close。
五、协程应用
理解生成器协程的实际应用:
生成器协程的应用:
① 累加器/运行统计(有状态):
def running_average():
total = 0; count = 0; avg = None
while True:
x = yield avg # 产出当前均值、接收新数据
total += x; count += 1
avg = total / count
ra = running_average(); next(ra)
ra.send(10) # 10.0
ra.send(20) # 15.0
→ 生成器保持状态,逐步接收数据、产出结果
② 状态机:
用 yield 暂停等待事件、send 注入事件、状态转移
③ 管道(数据处理链):
生成器串成管道,数据 send 进去、逐级处理
④ 可暂停/恢复的任务:
yield 暂停任务、send 恢复并传入新参数
⑤ 早期 asyncio 的基础:
yield 暂停等待 IO、send 恢复(现在用 async/await)
预激的处理(协程必做):
用装饰器自动预激:
@coroutine
def my_coro(): ... # 自动 next() 预激
协程 vs 迭代(区别):
迭代生成器:单向产出值(next/for)
协程生成器:双向(send 传入、yield 产出、throw/close 控制)
现代替代:
纯数据流 → 普通生成器 + yield
异步 IO → async/await(不用手写生成器协程)
→ 手写 send/throw 协程现在少见,但理解它是懂 async 前提
所以协程应用:累加器/状态机/管道/可暂停任务;预激用装饰器;现代异步用 async/await
生成器协程的应用:① 累加器/运行统计(有状态)(x = yield avg 产出当前均值、接收新数据、生成器保持状态逐步处理)、② 状态机(yield 暂停等待事件、send 注入事件、状态转移)、③ 管道(数据处理链)(生成器串成管道、数据 send 进去逐级处理)、④ 可暂停/恢复的任务、⑤ 早期 asyncio 的基础(yield 暂停等待 IO、send 恢复、现在用 async/await)。预激的处理(协程必做):用装饰器自动预激。协程 vs 迭代(区别):迭代生成器单向产出值(next/for)、协程生成器双向(send 传入、yield 产出、throw/close 控制)。现代替代:纯数据流用普通生成器、异步 IO 用 async/await(手写 send/throw 协程现在少见、但理解它是懂 async 前提)。理解「协程应用:累加器/状态机/管道/可暂停任务;预激用装饰器;协程双向(send/yield/throw/close)vs 迭代单向;现代异步用 async/await、理解生成器协程是懂 async 前提」,就掌握了协程应用。
六、总结与实践
总结三个方法:
核心:yield 是双向的(x = yield value)
value 产出给调用方、x 接收 send 的值
→ 生成器能当"协程"(接收+产出)
三个方法:
send(v):恢复+把 v 传给 yield 表达式+返回下个 yield
(next = send(None))
throw(e):在暂停处抛入异常(内部可 try/except)
close():抛入 GeneratorExit 请求结束(finally 清理)
关键规矩:
第一次必须 next()/send(None) 预激
(新生成器还没到第一个 yield,不能 send 非 None)
用途:
send:协程传值(累加器、状态机、管道)
throw:注入异常/错误信号
close:清理资源(finally 执行)
注意:
捕获 GeneratorExit 后不能再 yield(RuntimeError)
预激用装饰器自动化
现代异步用 async/await(不手写生成器协程)
核心总结:
send 传值进生成器(yield 表达式的值)+ 恢复
throw 抛异常进去、close 请求结束
第一次要预激,让生成器成为双向协程
所以 send 传值/throw 抛异常/close 结束,第一次预激,生成器成双向协程
核心:yield 是双向的(x = yield value,value 产出给调用方、x 接收 send 的值、生成器能当协程)。三个方法:send(v)(恢复+把 v 传给 yield 表达式+返回下个 yield,next = send(None))、throw(e)(暂停处抛入异常、内部可 try/except)、close()(抛入 GeneratorExit 请求结束、finally 清理)。关键规矩:第一次必须 next()/send(None) 预激。用途:send(协程传值)、throw(注入异常)、close(清理资源)。注意:捕获 GeneratorExit 后不能再 yield、预激用装饰器、现代异步用 async/await。理解「send 传值+恢复(next=send(None))/throw 抛异常/close 请求结束(finally 清理);第一次预激;用途累加器/状态机/清理;async/await 是现代替代」,就掌握了总结与实践。
记忆钩子:「普通生成器只『产出值』(next/for),但生成器是双向的——yield 既是产出出口也是接收入口(x=yield value:value 产出给调用方、x 在下次 send(v)时收到 v),通过三个方法让生成器成为『协程』:①send(value)——恢复生成器执行,把 value 作为『当前暂停的 yield 表达式的返回值』传进去(x=yield 里 x 收到 value),并返回下一个 yield 的值;★next(gen)等价 gen.send(None);②throw(exc)——在生成器当前暂停处『抛入』异常,内部可 try/except 处理(或传播回调用方),是调用方注入的控制信号;③close()——在暂停处抛入 GeneratorExit 异常请求结束,生成器的 finally 块会执行(清理资源),捕获后不能再 yield(否则 RuntimeError);★关键规矩:第一次必须先 next()或 send(None)『预激(prime)』,把新生成器推进到第一个 yield 暂停点,之后 send(有意义的值)才有 yield 表达式来接收,否则 send(非 None)报 TypeError;用途:协程(累加器/运行统计/状态机/数据管道)、可暂停恢复的任务、资源清理;常用自动预激装饰器;现代异步编程用 async/await 取代了手写生成器协程,但理解 send/yield 双向是懂 async 的前提」。
七、常见误区与追问
- 误区:yield 只能产出值,不能接收值。 yield 是双向的——
x = yield value里,value是产出给调用方的(用 next/for 取),而x会在调用方下次send(v)时收到v(yield 表达式的「返回值」来自 send);正是这个「既产出又接收」的特性,让生成器能当协程用(累加器、状态机等),调用方通过 send 把数据「喂」进生成器、生成器通过 yield 把结果「吐」出来。 - 误区:新创建的生成器可以直接 send(值)。 不行——新生成器停在「最开始」(还没执行到第一个 yield、没有暂停的 yield 表达式来接收值),直接
gen.send("x")(非 None)会抛TypeError: can't send non-None value to a just-started generator;必须先用next(gen)或gen.send(None)「预激(prime)」、把它推进到第一个 yield 暂停点,之后 send 有意义的值才有地方接收;协程代码常用一个装饰器自动完成预激。 - 误区:next(gen) 和 gen.send(None) 不一样。 完全等价——
next(gen)就是gen.send(None):都恢复生成器执行、让当前 yield 表达式的返回值为 None、并返回下一个 yield 产出的值;区别只是 send 还能传非 None 的值进去(send(v)让 yield 表达式返回 v);所以「预激」既可以用next(gen)也可以用gen.send(None)。 - 误区:close() 只是简单地让生成器停止,不用管资源。 恰恰相反——close() 的主要价值就是「触发资源清理」:它在生成器暂停处抛入
GeneratorExit异常,如果生成器里用了try/finally,finally 块会执行(关闭连接、释放锁、清理临时文件等);所以正确的资源型生成器应该把清理放在 finally 里、依赖 close(或 with contextlib.closing、或被垃圾回收时的自动 close)来触发;注意捕获了 GeneratorExit 后不能再 yield(否则抛RuntimeError: generator ignored GeneratorExit)、应该让生成器正常结束。 - 追问:send、throw、close 三个方法分别是干什么的,怎么配合? 它们是调用方向生成器发送三种「信号」:①
send(value)——恢复生成器执行、把 value 作为当前暂停的 yield 表达式的返回值传进去(x = yield里 x 收到 value)、并返回下一个 yield 产出的值,是「传数据进去」;②throw(exc)——在生成器当前暂停的 yield 处抛入一个异常,让生成器内部能 try/except 处理(或让异常传播回调用方),是「传错误信号进去」;③close()——在暂停处抛入 GeneratorExit 请求生成器结束、触发 finally 清理,是「传结束信号进去」;配合起来,生成器就成了一个能接收数据(send)、能处理注入的异常(throw)、能被优雅关闭并清理(close)的双向协程;使用前都要先 next() 预激。 - 追问:怎么用生成器实现一个「运行平均值」的协程? 用 send 传入数据、yield 产出当前均值:
def running_average(): total = 0; count = 0; avg = None; while True: x = yield avg; total += x; count += 1; avg = total / count——创建后先ra = running_average(); next(ra)预激(执行到第一个 yield、产出初始的 avg=None),之后每次ra.send(10)就把 10 传给 x、更新 total 和 count、算出新均值并通过 yield 产出(ra.send(10)返回 10.0、ra.send(20)返回 15.0);这个协程「记住」了 total 和 count 的状态、能持续接收数据流并实时产出统计结果,比每次重新计算高效;预激这一步可以用装饰器自动化。 - 追问:既然有生成器协程(send/throw),为什么还要 async/await? 生成器协程(用 yield + send/throw/close 手写)虽然能实现协程,但有明显问题:① 语法笨拙——要手动预激、要区分「产出值的 yield」和「等待的 yield」、双向语义容易混淆;② 和普通生成器(迭代用)语法上无法区分,容易搞错;③ 早期 asyncio 用「生成器 + yield from」实现异步,写起来繁琐、可读性差;Python 3.5 引入
async def/await(PEP 492)就是为了给「协程/异步」一套专门的、清晰的语法——async def明确声明这是协程、await明确表示「等待一个可等待对象」,语义清楚、不和迭代混淆、还能配合 async for / async with;所以现在异步编程一律用 async/await,手写 send/throw 生成器协程基本只在特殊场景或理解原理时才用;但理解「yield 双向 + send」是理解协程本质和 async/await 由来的基础。
八、加强记忆
普通生成器只「产出值」(next/for),但生成器是双向的——yield 既是产出出口也是接收入口(x = yield value:value 产出给调用方、x 在下次 send(v) 时收到 v),通过三个方法让生成器成为「协程」:① send(value)——恢复生成器执行、把 value 作为「当前暂停的 yield 表达式的返回值」传进去(x = yield 里 x 收到 value)、并返回下一个 yield 的值(next(gen) 等价 gen.send(None));② throw(exc)——在生成器当前暂停处「抛入」异常、内部可 try/except 处理(或传播回调用方);③ close()——在暂停处抛入 GeneratorExit 请求结束、生成器的 finally 块会执行(清理资源,捕获后不能再 yield 否则 RuntimeError)。关键规矩:第一次必须先 next() 或 send(None)「预激(prime)」、把新生成器推进到第一个 yield 暂停点,之后 send(有意义的值) 才有 yield 表达式接收,否则 send(非None) 报 TypeError。用途:协程(累加器/运行统计/状态机/数据管道)、可暂停恢复的任务、资源清理;现代异步编程用 async/await 取代了手写生成器协程,但理解 send/yield 双向是懂 async 的前提。一句话「yield 双向(x=yield value:产出 value、x 收 send 的值);send 传值进生成器+恢复(next=send(None))、throw 抛异常进暂停处、close 抛 GeneratorExit 请求结束(finally 清理);第一次必须 next 预激(否则 send 非 None 报 TypeError);让生成器成双向协程,现代异步用 async/await」。