Python 基础体系 · 第 16/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 生成器:yield、send、throw、close 与委托

生成器(generator)是能够暂停执行、保存现场,并在之后继续执行的迭代器。它既可以作为普通的惰性数据源,也可以通过 send()throw()close() 接收外部输入、异常与终止信号。

理解生成器不能只记住“yield 会返回一个值”。更准确的模型是:

生成器函数定义了一段可暂停的控制流;生成器对象保存这段控制流的运行现场;调用者通过 next()send()throw()close() 驱动它在不同状态之间转换。

Python 3.14 的语言参考将生成器函数、生成器对象和异步生成器分别定义在 yield 表达式与异步生成器相关章节中。普通生成器通过 __next__()send()throw()close() 与调用者交互。(docs.python.org)


生成器函数与生成器对象

包含 yield 表达式的普通函数会被定义为生成器函数

def numbers():
    yield 1
    yield 2
    yield 3

调用它时,函数体并不会立即执行:

iterator = numbers()

print(iterator)
print(type(iterator))

典型输出类似:

<generator object numbers at 0x...>
<class 'generator'>

这里有两个不同的对象:

  • numbers 是生成器函数;
  • numbers() 返回一个生成器对象;
  • 生成器对象同时也是迭代器,支持 __iter__()__next__()

调用生成器函数只创建并返回生成器对象。真正执行从第一次调用 next(iterator)iterator.send(None)iterator.throw(...)iterator.close() 开始。(docs.python.org)

def demo():
    print("开始执行")
    yield 10
    print("恢复执行")
    yield 20
    print("执行结束")

g = demo()

print("生成器已创建")
print(next(g))
print(next(g))

try:
    next(g)
except StopIteration:
    print("生成器已耗尽")

输出:

生成器已创建
开始执行
10
恢复执行
20
执行结束
生成器已耗尽

执行顺序是:

  1. demo() 创建生成器对象,没有打印任何内容;
  2. 第一次 next(g) 从函数开头执行;
  3. 执行到第一个 yield 10,产生 10 并暂停;
  4. 第二次 next(g) 从上一次暂停的位置继续;
  5. 执行到第二个 yield 20,产生 20 并再次暂停;
  6. 第三次 next(g) 继续执行,函数正常结束;
  7. 生成器以 StopIteration 表示耗尽。

暂停时,生成器会保留局部变量绑定、指令位置、求值栈以及异常处理状态,因此恢复后并不是重新调用函数,而是继续原来的执行现场。(docs.python.org)


yield 的双向数据流

最简单的写法是:

yield value

它向调用者产生 value。但是 yield 同时也是一个表达式:

received = yield produced

这行代码有两个方向的数据流:

生成器                         调用者
  |                              |
  | -- produced ---------------> |
  |                              |
  | <--------- send(value) ----- |
  |                              |
  | yield 表达式的结果为 value    |

第一次执行到 yield produced 时:

  • produced 被返回给调用者;
  • 生成器暂停;
  • received 尚未赋值。

当调用者之后使用 send(value) 恢复生成器时:

  • 当前 yield 表达式的结果变成 value
  • 因此 received == value
  • 生成器从赋值语句之后继续运行。

Python 官方文档将这种机制描述为:send(value) 传入的值会成为当前暂停位置的 yield 表达式的结果;如果使用 next() 恢复,结果则是 None。(docs.python.org)

完整示例

def accumulator():
    total = 0

    while True:
        value = yield total

        if value is None:
            return

        total += value


g = accumulator()

print(next(g))       # 启动生成器,并获取初始 total
print(g.send(5))     # 当前 yield 表达式得到 5
print(g.send(7))     # 当前 yield 表达式得到 7

输出:

0
5
12

逐步分析:

第一次调用 next(g)

生成器执行:

total = 0
value = yield total

yield total 产生 0,生成器暂停。此时:

total = 0
value 尚未赋值

调用 g.send(5)

send(5) 恢复上一次暂停的 yield 表达式:

value = 5

随后执行:

total += value

于是:

total = 5

循环再次执行到:

value = yield total

产生 5 并暂停。

调用 g.send(7)

同理:

value = 7
total = 5 + 7 = 12

下一次产生 12

因此,yield 可以看作一个“反向函数调用”:

result = function(argument)

通常是调用者传入 argument,函数返回 result。而生成器中的:

received = yield produced

则是:

  • 生成器先产生 produced
  • 调用者恢复时传入 value
  • 生成器内部得到 received

next()send(None) 的关系

对于已经暂停在某个 yield 处的生成器:

next(g)

等价于:

g.send(None)

两者都会让当前 yield 表达式的结果变成 None。(peps.python.org)

但是,生成器刚创建时还没有执行到任何 yield,不存在可以接收传入值的暂停点。因此,第一次启动只能使用:

next(g)

或者:

g.send(None)

不能直接传入非 None 值:

def echo():
    value = yield
    print(value)

g = echo()

g.send("hello")

结果是:

TypeError: can't send non-None value to a just-started generator

正确写法:

g = echo()

next(g)
g.send("hello")

这里的 next(g) 不表示“丢弃了一次业务数据”,而是完成了生成器的初始化,使它到达第一个能够接收输入的 yield


yieldreturnStopIteration

生成器中的 yield 表示暂时产生值并暂停,而 return 表示结束生成器

def sample():
    yield "first"
    return "final"

使用 next() 驱动:

g = sample()

print(next(g))

try:
    next(g)
except StopIteration as exc:
    print("异常类型:", type(exc).__name__)
    print("返回值:", exc.value)

输出:

first
异常类型: StopIteration
返回值: final

生成器执行 return value 后,会以 StopIteration(value) 结束;其中 value 可通过异常对象的 .value 属性取得。这个返回值通常在 yield from 委托中最有用。(peps.python.org)

需要区分两种“停止”:

def ordinary():
    yield 1

调用第二次 next() 时,生成器正常结束,抛出 StopIteration

但如果生成器内部直接写:

def invalid():
    yield 1
    raise StopIteration("bad")

在现代 Python 中,这不会被当成普通的生成器结束值。由生成器代码直接冒出的 StopIteration 会被转换为 RuntimeError,这是为了避免迭代器内部错误被误认为正常耗尽。(peps.python.org)

因此:

return "result"

是生成器返回最终结果的正确方式,而不是:

raise StopIteration("result")

生成器的状态

生成器至少可以处于以下四种可观察状态:

状态 含义
GEN_CREATED 已创建,但尚未开始执行
GEN_RUNNING 当前正在解释器中执行
GEN_SUSPENDED 暂停在某个 yield 表达式处
GEN_CLOSED 已正常结束、异常结束或被关闭

inspect.getgeneratorstate() 可以读取这些状态。(docs.python.org)

import inspect


def task():
    yield "ready"


g = task()

print(inspect.getgeneratorstate(g))
print(next(g))
print(inspect.getgeneratorstate(g))

try:
    next(g)
except StopIteration:
    pass

print(inspect.getgeneratorstate(g))

输出:

GEN_CREATED
ready
GEN_SUSPENDED
GEN_CLOSED

GEN_RUNNING 通常只能在生成器自身执行期间观察。例如:

import inspect


def show_state():
    print(inspect.getgeneratorstate(g))
    yield


g = show_state()
next(g)

在生成器执行到 print() 时,g 处于 GEN_RUNNING;执行到 yield 后变为 GEN_SUSPENDED

同一个生成器不能被并发或递归地重复驱动:

def recursive():
    yield g.send(None)

g = recursive()

如果生成器在自身执行期间再次调用自己的 send()throw()next(),通常会得到:

ValueError: generator already executing

这不是线程同步机制。生成器的暂停与恢复提供的是控制流机制,不会自动让多个线程安全地共享一个生成器。


send():向暂停点传入值

send(value) 的协议可以形式化为:

设生成器当前暂停在:

result = yield produced

则:

g.send(value)

执行后相当于让:

result = value

然后继续运行,直到:

  1. 再次遇到 yield,返回新的产生值;
  2. 生成器结束,抛出 StopIteration
  3. 生成器抛出未处理异常,该异常传播给调用者。

示例:

def protocol():
    print("step 1")
    command = yield "waiting"

    print("received:", command)

    if command == "stop":
        return "stopped"

    yield "continue"
    return "finished"


g = protocol()

print(next(g))
print(g.send("go"))

try:
    g.send("stop")
except StopIteration as exc:
    print("result:", exc.value)

输出:

step 1
waiting
received: go
continue
result: stopped

注意,send() 的返回值不是“生成器的返回值”,而是生成器恢复后下一次 yield 产生的值。只有生成器结束并抛出 StopIteration 时,才通过异常的 .value 获取 return 的结果。

一个常见误解

下面两段代码并不等价:

yield value

和:

result = yield value

前者忽略恢复时传入的值;后者会把该值接收下来。

def ignore_input():
    while True:
        yield "constant"


def receive_input():
    while True:
        value = yield "waiting"
        print("got:", value)

调用:

g1 = ignore_input()
next(g1)
print(g1.send(123))       # 仍然产生 constant

g2 = receive_input()
next(g2)
print(g2.send(123))       # 打印 got: 123,然后产生 waiting

send() 只是向当前 yield 表达式提供结果。生成器是否使用这个结果,取决于生成器代码本身。


throw():在暂停点注入异常

throw() 不会在调用者当前位置执行 raise,而是恢复生成器,并在生成器上一次暂停的 yield 位置引发指定异常。

def resilient():
    while True:
        try:
            value = yield "ready"
        except ValueError as exc:
            print("handled:", exc)
            yield "recovered"


g = resilient()

print(next(g))
print(g.throw(ValueError("bad input")))
print(g.send(10))

输出:

ready
handled: bad input
recovered
ready

过程如下:

  1. next(g) 使生成器暂停在:

    value = yield "ready"
    
  2. g.throw(ValueError("bad input")) 在这个 yield 位置引发异常;

  3. 异常被 except ValueError 捕获;

  4. 生成器执行 yield "recovered"

  5. throw() 返回 "recovered"

  6. 下一次 send(10) 从新的 yield 恢复;

  7. 循环回到原来的位置,再次产生 "ready"

如果生成器不捕获这个异常,异常就会传播到 throw() 的调用者:

def simple():
    yield "ready"


g = simple()
next(g)

try:
    g.throw(RuntimeError("failure"))
except RuntimeError as exc:
    print("caller caught:", exc)

输出:

caller caught: failure

如果生成器在处理异常后正常结束,throw() 会抛出 StopIteration,而不是返回一个普通值:

def stop_on_error():
    try:
        yield "ready"
    except ValueError:
        return "done"


g = stop_on_error()
next(g)

try:
    g.throw(ValueError("invalid"))
except StopIteration as exc:
    print(exc.value)

输出:

done

throw() 的常用形式是传入异常实例:

g.throw(ValueError("invalid"))

Python 3.14 仍支持历史形式:

g.throw(ValueError, ValueError("invalid"), traceback)

但三参数形式已经在 Python 3.12 中标记为弃用,新的代码应优先使用异常实例形式。(docs.python.org)

向尚未启动的生成器调用 throw()

throw() 可以作用于尚未启动的生成器,但异常会在生成器函数入口处注入。生成器如果没有在入口附近捕获它,就不会执行到后面的代码:

def startup():
    try:
        yield "ready"
    except ValueError:
        yield "recovered"


g = startup()
print(g.throw(ValueError("before start")))

输出:

recovered

这与 send() 不同:send(non_none) 在尚未启动时非法,而 throw() 可以直接把异常送入生成器。


close():请求生成器终止

close() 用于向生成器注入特殊异常 GeneratorExit

g.close()

语义上近似于:

g.throw(GeneratorExit)

close()GeneratorExit 有专门的处理规则:

  • 生成器抛出 GeneratorExit,关闭成功;
  • 生成器正常结束,关闭成功;
  • 生成器在收到 GeneratorExit 后又产生值,抛出 RuntimeError
  • 生成器抛出其他异常,该异常传播给调用者;
  • 已经结束的生成器再次 close(),通常什么也不做并返回 None

这些规则由 Python 语言参考明确规定。Python 3.13 起,如果生成器在关闭过程中通过 return value 返回值,close() 会返回该值;这属于 Python 3.13 引入的行为,在 Python 3.14 中仍然适用。(docs.python.org)

使用 finally 清理资源

def resource_user():
    print("acquire")

    try:
        yield "using"
    finally:
        print("release")


g = resource_user()

print(next(g))
g.close()

输出:

acquire
using
release

close() 在暂停的 yield 位置引发 GeneratorExit,因此控制流会进入 finally。这使生成器可以把清理逻辑放在 finally 中。

不应吞掉 GeneratorExit 后继续产生值

错误示例:

def broken():
    try:
        yield "data"
    except GeneratorExit:
        yield "another-data"


g = broken()
print(next(g))

try:
    g.close()
except RuntimeError as exc:
    print(type(exc).__name__, exc)

输出类似:

RuntimeError generator ignored GeneratorExit

收到 GeneratorExit 后,生成器只能:

def correct():
    try:
        yield "data"
    finally:
        print("cleanup")

或者显式重新抛出:

def also_correct():
    try:
        yield "data"
    except GeneratorExit:
        print("cleanup")
        raise

不能捕获 GeneratorExit 后继续执行普通的 yield。否则调用者已经要求生成器终止,而生成器却重新向外产生数据,协议状态就发生冲突。


close() 与垃圾回收不是同一件事

生成器对象被垃圾回收时,Python 可能调用它的 close(),从而使挂起的 finally 有机会执行。但是,“对象不再被引用”与“清理立即发生”不是所有 Python 实现都保证的等价关系。

CPython 通常使用引用计数,因此最后一个引用消失后往往很快触发清理;但这属于常见实现行为,不应当被当成跨实现的及时性保证。非引用计数实现可能延迟垃圾回收,循环引用也可能延迟生成器终结。(peps.python.org)

因此,涉及文件、锁、数据库连接或事务时,应显式关闭生成器,或把生命周期放进 try/finally、上下文管理器等明确结构中:

g = resource_user()

try:
    print(next(g))
finally:
    g.close()

生成器的自动终结适合作为兜底机制,不适合作为主要资源管理协议。


yield from:把控制权委托给下层迭代器

yield from iterable 称为委托(delegation)

它不是简单的:

for item in iterable:
    yield item

在只使用 next() 的情况下,两者经常表现相同:

def manual():
    for item in [1, 2, 3]:
        yield item


def delegated():
    yield from [1, 2, 3]

但是,一旦使用 send()throw()close() 或下层生成器的返回值,两者的语义就不同。

yield from 会建立一个“委托生成器—子迭代器”关系:

调用者
  |
  | next / send / throw / close
  v
委托生成器
  |
  | 转发控制操作
  v
子迭代器

Python 语言参考规定:

  • 子迭代器产生的值直接交给外部调用者;
  • 外部 send() 的值转发给子迭代器;
  • 外部 throw() 的异常转发给子迭代器的 throw()
  • 外部关闭委托生成器时,若子迭代器有 close(),也会先关闭子迭代器;
  • 子迭代器结束时,其 StopIteration.value 成为 yield from 表达式的结果。(peps.python.org)

yield from 的返回值

先看子生成器:

def child():
    yield "child: one"
    yield "child: two"
    return 42

委托生成器:

def parent():
    result = yield from child()
    print("child returned:", result)
    yield "parent: done"


g = parent()

print(next(g))
print(next(g))
print(next(g))

输出:

child: one
child: two
child returned: 42
parent: done

执行过程:

  1. next(g) 进入 parent()
  2. 执行 yield from child(),创建并驱动子生成器;
  3. 子生成器产生 "child: one",直接返回给调用者;
  4. 下一次 next(g) 继续子生成器;
  5. 子生成器产生 "child: two"
  6. 再次 next(g) 时,子生成器执行 return 42
  7. 子生成器结束并产生 StopIteration(42)
  8. yield from child() 表达式的结果变为 42
  9. result 被赋值为 42
  10. 父生成器打印结果并产生 "parent: done"

因此,yield from 同时传递两类信息:

  • 子迭代器的中间产出值;
  • 子迭代器结束时的最终返回值。

这正是普通 for ... yield 委托无法自动提供的部分。


yield from 的形式化展开

下面的代码:

result = yield from iterable

可以近似理解为以下协议循环:

iterator = iter(iterable)

try:
    value = next(iterator)
except StopIteration as exc:
    result = exc.value
else:
    while True:
        try:
            sent = yield value
        except GeneratorExit:
            close = getattr(iterator, "close", None)
            if close is not None:
                close()
            raise
        except BaseException as exc:
            throw = getattr(iterator, "throw", None)

            if throw is None:
                raise

            try:
                value = throw(type(exc), exc, exc.__traceback__)
            except StopIteration as exc:
                result = exc.value
                break
        else:
            try:
                if sent is None:
                    value = next(iterator)
                else:
                    value = iterator.send(sent)
            except StopIteration as exc:
                result = exc.value
                break

这个展开揭示了几个关键条件。

普通 next() 的转发

外部调用:

next(parent_generator)

最终会使子迭代器执行:

next(iterator)

send(None) 的转发

如果外部调用:

parent_generator.send(None)

委托逻辑仍然调用:

next(iterator)

因为 send(None) 在生成器协议中等价于无参数恢复。

send(value) 的转发

如果外部调用:

parent_generator.send(value)

其中 value is not None,委托逻辑会尝试调用:

iterator.send(value)

因此,子迭代器必须支持 send(),否则会出现属性错误或类型错误。语言参考也明确指出,yield from 对下层对象的 send()throw() 支持取决于该对象是否提供相应方法。(docs.python.org)

throw() 的转发

外部注入的普通异常会优先传给子迭代器的 throw()

parent_generator.throw(ValueError("bad"))

如果子迭代器没有 throw() 方法,则该异常直接回到委托生成器。

close() 的转发

外部执行:

parent_generator.close()

如果委托生成器当前正在 yield from 子迭代器,并且子迭代器有 close(),委托逻辑会调用它,然后继续终止外层生成器。

这使 yield from 不只是值转发机制,也是完整的控制协议转发机制。PEP 380 的设计目标之一,就是让代码拆分为父生成器和子生成器后,在 next()send()throw()close() 等行为上尽可能保持与未拆分代码一致。(peps.python.org)


yield from 与普通 for 委托的差异

比较下面两种写法:

def with_for(subgenerator):
    for item in subgenerator:
        yield item


def with_yield_from(subgenerator):
    yield from subgenerator

如果调用者只做:

for item in generator:
    ...

两者可能得到相同的值。

但如果调用者通过 send() 交互:

def child():
    command = yield "ready"
    print("child received:", command)
    yield "done"


def parent_for():
    for item in child():
        yield item


def parent_from():
    yield from child()

parent_for()

g = parent_for()
print(next(g))
print(g.send("hello"))

"hello" 发送给的是 parent_for() 当前的 yield item。父生成器没有把这个值继续传给子生成器,子生成器内部的:

command = yield "ready"

得到的是 None

而对 parent_from()

g = parent_from()
print(next(g))
print(g.send("hello"))

"hello" 会被 yield from 转发给 child(),因此子生成器会打印:

child received: hello

这就是:

for + yield

与:

yield from

的本质区别:前者只转发产出值,后者转发完整的生成器控制协议。


委托给普通可迭代对象

yield from 的目标不一定是生成器,也可以是列表、元组、文件对象或任意可迭代对象:

def combined():
    yield from [1, 2]
    yield from (3, 4)
    yield from "ab"


print(list(combined()))

输出:

[1, 2, 3, 4, 'a', 'b']

但是普通列表迭代器通常没有:

send()
throw()
close()

因此,yield from 对它们的高级控制能力有限:

  • 普通 next() 可以正常转发;
  • Nonesend() 可能因为目标迭代器没有 send() 而失败;
  • throw() 不会像生成器那样进入列表迭代器内部;
  • close() 只有在下层迭代器存在 close() 时才会转发。

所以,“yield from 可以委托给任意可迭代对象”与“任意可迭代对象都支持完整生成器协议”是两件不同的事。


异常、关闭与委托链

考虑三层生成器:

def leaf():
    try:
        while True:
            try:
                command = yield "leaf: ready"
            except ValueError:
                yield "leaf: recovered"
    finally:
        print("leaf: cleanup")


def middle():
    result = yield from leaf()
    print("middle result:", result)


def root():
    yield from middle()

驱动过程:

g = root()

print(next(g))
print(g.throw(ValueError("temporary")))
g.close()

输出:

leaf: ready
leaf: recovered
leaf: cleanup

路径是:

  1. next(root) 进入 middle
  2. middle 委托给 leaf
  3. leaf 产生 "leaf: ready"
  4. root.throw(ValueError(...)) 沿委托链传入 middle,再传入 leaf
  5. leaf 捕获异常并产生 "leaf: recovered"
  6. root.close() 沿委托链向下传播;
  7. leaf.close() 被调用;
  8. leaffinally 执行;
  9. GeneratorExit 从内层向外层终止整条委托链。

委托链可以把复杂任务拆分为多个生成器,同时保留异常和关闭的传播路径。它的代价是:一旦下层生成器被多个上层对象共享,关闭其中一个委托者可能会关闭共享的下层对象。PEP 380 也明确讨论了这种共享子迭代器带来的生命周期问题。(peps.python.org)


tryfinally 与生成器生命周期

yield 可以出现在 try 结构中:

def stream():
    try:
        yield "item"
    finally:
        print("stream closed")

但这并不意味着 finally 一定在生成器对象创建后立即执行,也不意味着只要调用者停止使用生成器,清理就马上发生。

下面代码中,finally 只有在生成器继续执行、显式关闭、或最终化机制介入时才会执行:

g = stream()

print("created")
# 此时 stream() 的函数体还未开始执行

print(next(g))
# 此时暂停在 yield,finally 尚未执行

g.close()
# 此时 finally 执行

如果生成器需要持有资源,应该明确设计谁负责调用 close()。否则,生成器的暂停可能长期保留资源引用。

一个常见的资源封装方式是把生成器放入上下文管理器:

from contextlib import contextmanager


@contextmanager
def managed():
    print("open")
    try:
        yield "resource"
    finally:
        print("close")


with managed() as resource:
    print(resource)

输出:

open
resource
close

这里的 yield 并不是普通数据流中的任意暂停点,而是上下文管理器把“进入上下文”和“离开上下文”连接起来的控制点。


生成器不是线程,也不是 asyncio 协程

生成器与协程有相似之处:

  • 都能暂停;
  • 都保存执行现场;
  • 都能在之后恢复;
  • 都可以通过某种协议与调度者交互。

但是普通生成器本身不会自动等待 I/O,也不会自动加入事件循环。

def generator():
    yield "paused"


async def coroutine():
    await some_operation()

普通生成器的暂停点由 yield 建立,调用者通过 next()send() 等方法显式驱动;async def 创建的是原生协程,暂停点通常由 await 建立,并由异步运行时调度。

此外,在 async def 协程函数体中使用 yield from 是语法错误;异步生成器使用的是异步迭代协议,例如 async for__anext__() 以及对应的异步发送和关闭机制。(docs.python.org)

因此,下面两种代码不能混为一谈:

def legacy_style():
    result = yield from subgenerator()
async def modern_style():
    result = await coroutine()

前者是普通生成器的委托;后者是原生协程的等待。它们在语法外观上都涉及暂停和恢复,但协议、调度器和异常终止机制不同。


一个完整的交互式生成器

下面实现一个简化的批处理生成器:

  • 通过 send() 接收任务;
  • 通过普通 yield 报告结果;
  • 通过 throw() 注入可恢复错误;
  • 通过 close() 执行清理;
  • 通过 return 返回最终统计结果。
def worker():
    processed = 0

    try:
        while True:
            try:
                task = yield {"status": "waiting", "processed": processed}
            except ValueError as exc:
                yield {
                    "status": "error",
                    "message": str(exc),
                    "processed": processed,
                }
                continue

            if task is None:
                return {"processed": processed, "reason": "normal stop"}

            if task == "stop":
                return {"processed": processed, "reason": "requested stop"}

            processed += 1
            yield {
                "status": "done",
                "task": task,
                "processed": processed,
            }

    finally:
        print("worker cleanup")

驱动它:

g = worker()

# 启动,进入第一个 yield
print(next(g))

# 向当前 yield 发送任务
print(g.send("task-1"))

# 恢复到下一个等待点
print(next(g))

# 注入可恢复异常
print(g.throw(ValueError("bad task")))

# 恢复到等待点
print(next(g))

# 请求正常结束
try:
    g.send("stop")
except StopIteration as exc:
    print("final result:", exc.value)

输出结构类似:

{'status': 'waiting', 'processed': 0}
{'status': 'done', 'task': 'task-1', 'processed': 1}
{'status': 'waiting', 'processed': 1}
{'status': 'error', 'message': 'bad task', 'processed': 1}
{'status': 'waiting', 'processed': 1}
worker cleanup
final result: {'processed': 1, 'reason': 'requested stop'}

这个例子中的关键点是:

  1. 生成器启动后先停在“等待任务”的 yield
  2. send("task-1") 让该 yield 表达式的结果变为 "task-1"
  3. 处理任务后,生成器在另一个 yield 处暂停并返回任务结果;
  4. 下一次 next() 使它回到等待位置;
  5. throw(ValueError(...)) 在等待任务的 yield 处触发异常;
  6. 异常被内部处理后,生成器产生错误报告;
  7. send("stop") 使生成器执行 return
  8. return 的值通过 StopIteration.value 交给调用者;
  9. 无论正常停止还是异常关闭,finally 都承担清理责任。

这个协议适合表达“由外部调度者驱动的状态机”。但它并不自动提供消息队列、线程安全、背压、超时或取消语义。如果多个线程同时操作同一个生成器,需要在生成器外部建立同步和生命周期管理。


诊断生成器问题的方法

检查是否为生成器函数

import inspect


def source():
    yield 1


print(inspect.isgeneratorfunction(source))

输出:

True

检查是否为生成器对象

g = source()

print(inspect.isgenerator(g))

输出:

True

检查当前状态

print(inspect.getgeneratorstate(g))
next(g)
print(inspect.getgeneratorstate(g))

检查是否正在委托

生成器对象有一个常见的内省属性:

g.gi_yieldfrom

当生成器当前没有委托给下层对象时,通常为 None;执行到 yield from 并暂停在下层迭代器时,可以观察到对应的委托对象。gi_yieldfrom 是生成器对象提供的内省属性,Python 文档记载它自 Python 3.5 起可用。(docs.python.org)

常见失败与定位方向

1. 忘记启动就调用 send(value)

TypeError: can't send non-None value to a just-started generator

修复方式:

next(g)
g.send(value)

2. 把 send() 的返回值当成最终返回值

send() 返回的是生成器下一次 yield 的值;生成器的 return 值要从 StopIteration.value 读取。

3. 在 GeneratorExit 后继续 yield

这会触发:

RuntimeError: generator ignored GeneratorExit

修复方式是在清理后结束生成器,不要继续产生值。

4. 在生成器内部直接抛出 StopIteration

现代 Python 会将其转换为 RuntimeError。使用 return value 表示正常结束。

5. 以为 yield from 只是语法缩写

对于简单的 next() 遍历,它类似于 for ...: yield ...;对于 send()throw()close() 和最终返回值,它是更完整的协议委托。

6. 重复驱动同一个生成器

生成器不是可重入对象。生成器执行期间再次驱动它,会触发“generator already executing”类错误。应当由一个明确的调度者顺序驱动,或为每个独立任务创建不同的生成器实例。


规范保证、实现行为与工程取舍

规范保证

Python 3.14 语言层面保证:

  • 使用 yield 的函数是生成器函数;
  • 生成器对象在 yield 处暂停并保存执行状态;
  • send() 将值作为当前 yield 表达式的结果;
  • throw() 在暂停点引发异常;
  • close() 通过 GeneratorExit 请求终止;
  • yield from 转发下层迭代器的值和部分控制操作;
  • 子迭代器的 StopIteration.value 成为 yield from 表达式的结果。(docs.python.org)

常见实现行为

在 CPython 中:

  • 生成器通常持有可观察的 Python 栈帧;
  • 引用计数经常使对象失去最后引用后较快触发最终化;
  • gi_framegi_yieldfrom 等属性可用于诊断。

但具体栈帧暴露方式、垃圾回收时机和对象销毁时机不应被当作所有 Python 实现都相同的语义保证。inspect.getgeneratorlocals() 文档也明确指出,它依赖 Python 栈帧暴露能力,在某些实现中可能只能返回空字典。(docs.python.org)

工程取舍

如果需求只是惰性地产生一系列值,普通 yield 通常足够:

def read_rows():
    for row in source:
        yield transform(row)

如果需求包含“向暂停计算发送结果”,才需要:

result = yield request

如果需求包含“从外部取消或通知异常”,可以考虑:

try:
    command = yield state
except Cancelled:
    ...

如果需要把复杂控制流拆分到多个生成器,同时保留值、输入、异常和关闭传播,应使用:

result = yield from subgenerator()

如果需求是现代异步 I/O、事件循环调度或任务取消,优先使用 async defawaitasyncio 的原生协程体系,而不是自行用普通生成器模拟调度器。普通生成器仍然适合表达同步的惰性计算、解析器状态机、数据管线和可控的协作式控制流,但它不会自动提供异步运行时能力。

生成器真正的核心不是“少创建一个列表”,而是建立了一个可暂停的执行协议:

next()       —— 继续执行,不传入业务值
send(value)  —— 继续执行,并让 yield 表达式得到 value
throw(exc)   —— 在暂停点引发异常
close()      —— 请求以 GeneratorExit 终止
yield from   —— 将这些控制操作委托给下层迭代器

掌握这组协议后,yield 不再只是一个惰性循环语法,而是一种完整的双向控制流机制。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。