在 Python 中,生成器对象在迭代语义中只是一个普通的迭代器,解释器只通过 __iter__ 与 __next__ 推进它的执行。但与此同时,生成器对象具备一些额外的扩展接口,用于对其执行过程进行显式控制。这些接口包括:send()、throw() 与 close()。它们的共同特点是:解释器在迭代语境中不会自动调用它们,它们只服务于用户对执行过程的主动干预。
一、生成器对象的扩展接口不属于迭代协议
在讨论具体接口之前,必须先明确一个边界事实:生成器对象之所以能被 for、next() 等机制驱动,完全是因为它实现了迭代协议。扩展接口的存在与否,不会影响协议是否成立。
也就是说,解释器在任何迭代语义中,都不会检查生成器对象是否具有 send、throw 或 close。
比如:
def gen(): yield 1 yield 2g = gen()# for 循环只会调用 __iter__ 和 __next__for x in g: print(x) # 1 2
在整个过程中,解释器既不知道、也不关心生成器是否支持 send() 或 throw()。
二、send(value):向暂停点注入值
send(value) 用于恢复生成器的执行,并将 value 作为上一次暂停处 yield 表达式的结果。
这一接口成立的前提是:生成器必须已经执行到至少一个 yield,从而形成一个可恢复的暂停点。
示例:yield 作为表达式接收外部值
def gen(): x = yield 1 # 第一次暂停点 yield x # 使用 send 传入的值g = gen()next(g) # 产出 1,执行帧被冻结在 yield 1print(g.send("hi")) # "hi",作为 yield 1 表达式的结果赋给 x
这里的关键不在于“生成器能通信”,而在于 yield 暂停的执行帧,在恢复时可以接收一个外部提供的值。
一个常见误解是认为 send() 是“next 的增强版”。事实上,send() 的使用受到严格的执行状态约束。
在生成器尚未启动之前,没有执行帧、没有暂停点,也不存在可接收值的 yield 表达式。
因此,首次恢复只能使用 next() 或 send(None)。
示例:未启动生成器时不能直接 send
def gen(): x = yield 1 yield xg = gen()# g.send("hi") # TypeError:执行帧尚未建立next(g) # 正确启动生成器g.send("hi") # 合法
这一限制不是语法规则,而是执行模型的直接结果。
三、throw(exc):向执行帧内部注入异常
throw(exc) 用于在生成器当前暂停点,向其执行帧内部抛出一个异常,就好像该异常是在生成器内部发生的一样。
生成器可以选择捕获该异常,或者让其向外传播。
示例:生成器内部捕获外部注入的异常
def gen(): try: yield 1 except ValueError: yield "handled"g = gen()print(next(g)) # 1print(g.throw(ValueError)) # "handled"
从执行语义上看,throw() 所做的只是恢复执行帧,并在恢复点立刻触发一次异常。
对尚未启动的生成器调用 throw() 时,由于尚不存在可注入的暂停点,异常通常会直接向外传播。要让异常在生成器内部被捕获,必须先 next() / send(None) 将其推进到一个暂停点。
如果通过 throw() 注入的异常在生成器内部未被捕获,那么该异常会终止生成器的执行,并向外传播。
示例:异常导致生成器终止
def gen(): yield 1g = gen()next(g)g.throw(RuntimeError) # RuntimeError 向外抛出,生成器终止
这与普通函数中未捕获异常的行为完全一致,体现了生成器执行模型与普通函数的一致性。
四、close():请求终止执行过程
close() 用于显式请求生成器结束执行,其效果是:向执行帧注入终止信号,使其进入关闭流程。若后续再推进将直接触发 StopIteration。
示例:主动关闭生成器
def gen(): yield 1 yield 2g = gen()print(next(g)) # 1g.close()next(g) # StopIteration
close() 并不是“清空生成器”,而是请求当前执行过程自行结束,并使其进入不可再推进的终止态。
在生成器内部,close() 的语义等价于抛出 GeneratorExit 异常。
该异常的特殊之处在于:生成器在响应关闭请求时,不允许再产出值。
示例:在 GeneratorExit 中继续 yield 是非法的
def gen(): try: yield 1 except GeneratorExit: yield 2 # 运行时错误:关闭阶段不允许继续产出
这一限制明确传达了设计意图:关闭是终止信号,而不是一次普通的异常处理分支。
五、生成器扩展接口的应用示例
当生成器不再只是“被动产出值”,而需要与外部形成“可控的执行协作”时,生成器对象的扩展接口就可真正发挥价值。
也就是说,当执行过程本身成为交互对象,而不仅是数据来源时,扩展接口才具有不可替代的意义。
示例:可中断、可反馈的任务执行器
def task(): try: # 第一步:准备阶段 result = yield "prepare" # 第二步:根据外部反馈决定是否继续 if not result: yield "aborted" return # 第三步:正式执行 yield "running" # 第四步:执行完成 yield "done" except RuntimeError: # 外部强制中断 yield "error"
使用方:
t = task()# 启动任务print(next(t)) # "prepare"# 外部反馈:准备成功print(t.send(True)) # "running"# 外部注入异常,强制改道执行流print(t.throw(RuntimeError)) # "error"# "error" 是最后一次 yield;从该暂停点继续执行将直接结束next(t) # StopIteration
说明:
1、next():用于建立执行帧并进入第一个暂停点,这是生成器的启动步骤。
2、send(value):用于将外部状态注入执行过程,使生成器的执行路径能够根据外部反馈发生变化。
3、throw(exc):用于将异常语义直接注入生成器内部,使其以“自身异常”的方式改变执行流。
4、close()(此处未显式调用):在任务被彻底放弃时,可用于立即终止执行并释放执行帧。
需要注意的是,整个过程中,解释器并未参与任何决策,所有控制都发生在用户代码、生成器对象、执行帧之间。
这个示例揭示了一个关键设计事实:生成器扩展接口并不是为了“增强迭代”,而是为了让“执行过程本身”成为可被操控的对象。
在这种模型下,生成器不再只是“数据的生产者”,而是一个协作式执行单元;外部代码不再只是消费者,而是参与者。
📘 小结
生成器对象在迭代协议之外,额外提供了 send()、throw() 与 close() 等扩展接口,用于对其可暂停执行过程进行显式控制。这些接口并不属于解释器在任何语法语境下自动启用的语言级协议,解释器在迭代语义中也不会主动使用它们。它们的存在并非为了扩展迭代规则,而是为了在保持协议最小化的前提下,为“执行过程对象化”这一设计提供更高层次的运行期控制能力。