11.6 生成器

Python 生成器是返回 yield 语句的函数,其可以不断被调用以生成更多值。

生成器通常用作一种更节省内存的方式来循环遍历大数据块(如文件、数据库或网络)中的值。当使用 yield 而不是 return 时,将返回生成器对象来代替。生成器对象从 yield 语句创建并返回给调用者。

这个简单的生成器函数将生成字母 az

cpython-book-samples/33/letter_generator.py

def letters():
    i = 97 # Letter 'a' in ASCII
    end = 97 + 26 # Letter 'z' in ASCII
    while i < end:
        yield chr(i)
        i += 1

如果你调用 letters(),那么它不会返回值。相反,它将返回一个生成器对象:

>>> from letter_generator import letters
>>> letters()
<generator object letters at 0x1004d39b0>

for 语句的语法内置了一种能力——迭代生成器对象直到它停止产生值:

>>> for letter in letters():
...     print(letter)
a
b
c
d
...

此实现使用了迭代器协议。具有 __next__() 方法的对象可以通过 forwhile 循环或使用内置的 next() 进行循环。

Python 中的所有容器类型(如列表、集合和元组)都实现了迭代器协议。生成器的特殊之处在于,其 __next__() 方法的实现会从其最后状态重新调用生成器函数。

生成器不在后台执行——它们被暂停了。当你请求另一个值时,它们会恢复执行。在生成器对象结构中的是 frame 对象,就像在上一个 yield 语句中的一样。

生成器结构

生成器对象由模板宏 _PyGenObject_HEAD(prefix) 创建。

该宏由以下类型和前缀使用:

  • 生成器对象:PyGenObject (gi_)

  • 协程对象:PyCoroObject (cr_)

  • 异步生成器对象PyAsyncGenObject (ag_)

你将在本章后面了解协程和异步生成器对象。

PyGenObject 类型具有以下基础属性:

名称类型用途

[x]_code

PyObject * (PyCodeObject*)

生成生成器的编译函数

[x]_exc_state

_PyErr_StackItem

生成器调用引发异常时的异常数据

[x]_frame

PyFrameObject*

生成器的当前 frame 对象

[x]_name

PyObject * (str)

生成器名称

[x]_qualname

PyObject * (str)

生成器限定名称

[x]_running

char

如果生成器当前正在运行,则设置为 0,否则为 1

[x]_weakreflist

PyObject * (list)

生成器函数内部对象的弱引用列表

在基础属性之上,PyCoroObject 类型具有以下属性:

名称类型用途

cr_origin

PyObject * (tuple)

包含原始帧和调用者的元组

在基础属性之上,PyAsyncGenObject 类型具有以下属性:

名称类型用途

ag_closed

int

标记生成器已关闭的标志

ag_finalizer

PyObject *

链接到终结器方法

ag_hooks_inited

int

标记钩子已经初始化的标志

ag_running_async

int

标记生成器正在运行的标志

相关源文件

以下是与生成器相关的源文件:

文件用途

Include/genobject.h

生成器 API 和 PyGenObject 定义

Objects genobject.c

生成器对象实现

创建生成器

当编译包含 yield 语句的函数时,生成的 code 对象有一个额外的标志,CO_GENERATOR

在求值循环一章的“构造 frame 对象”部分,你探索了编译的 code 对象在执行时如何转换为 frame 对象。在这个过程中,生成器、协程和异步生成器有一个特例。

_PyEval_EvalCode() 检查 code 对象中的 CO_GENERATORCO_COROUTINECO_ASYNC_GENERATOR 标志。如果它找到这些标志中的任何一个,将不会直接执行 code 对象,而是分别使用 PyGen_NewWithQualName()PyCoro_New()PyAsyncGen_New() 创建一个帧,并将其变成相应的生成器、协程或异步生成器:

PyObject *
_PyEval_EvalCode(PyObject *_co, PyObject *globals, PyObject *locals, ...
...
    /* Handle generator/coroutine/asynchronous generator */
    if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
        PyObject *gen;
        PyObject *coro_wrapper = tstate->coroutine_wrapper;
        int is_coro = co->co_flags & CO_COROUTINE;
        ...
        /* Create a new generator that owns the ready-to-run frame
        * and return that as the value. */
        if (is_coro) {
>>>         gen = PyCoro_New(f, name, qualname);
        } else if (co->co_flags & CO_ASYNC_GENERATOR) {
>>>         gen = PyAsyncGen_New(f, name, qualname);
        } else {
>>>         gen = PyGen_NewWithQualName(f, name, qualname);
        }
        ...
        return gen;
    }
...

生成器工厂 PyGen_NewWithQualName() 获取帧并完成一些步骤来填充生成器对象字段:

  1. gi_code 属性设置为编译后的 code 对象;

  2. 将生成器设置为未运行(gi_running = 0);

  3. 将异常和弱引用列表设置为 NULL

还可以通过导入 dis 模块,反汇编里面的字节码,看到 gi_code 是生成器函数编译后的 code 对象:

>>> from letter_generator import letters
>>> gen = letters()
>>> import dis
>>> dis.disco(gen.gi_code)
  2           0 LOAD_CONST              1 (97)
              2 STORE_FAST              0 (i)
...

在有关求值循环的章节中,你探索了 frame 对象类型。frame 对象包含局部变量和全局变量、最后执行的指令以及要执行的代码。

frame 对象的内置行为和状态允许生成器按需暂停和恢复。

执行生成器

每当在生成器对象上调用 __next__() 时,生成器实例就会调用 gen_iternext(),它会立即调用 Objects/genobject.c 中的 gen_send_ex()

gen_send_ex() 是将生成器对象转换为下一个生成结果的函数。你会发现这与从 code 对象构造帧的方式有许多相似之处,因为这些函数具有相似的任务。

gen_send_ex() 与生成器、协程和异步生成器共享,具有以下步骤:

  1. 获取当前线程状态;

  2. 从生成器对象中获取 frame 对象;

  3. 如果在调用 __next__() 时生成器正在运行,则引发 ValueError

  4. 如果生成器内部的帧在栈顶:

    • 如果这是协程,并且协程尚未标记为关闭,则会引发 RuntimeError

    • 如果这是一个异步生成器,则会引发 StopAsyncIteration

    • 如果这是一个标准生成器,则会引发一个 StopIteration

  5. 如果帧中的最后一条指令 (f->f_lasti) 仍然是 -1,那是因为它才刚刚开始,并且如果这是协程或异步生成器,那么除了 None 之外的任何值都不能作为参数传递,并引发异常;

  6. 否则,这是第一次调用,并且允许传递参数。参数的值被推送到帧的值栈中;

  7. 帧的 f_back 字段表示要将返回值发送给的调用者,所以它被设置为线程中的当前帧。这意味着返回值被发送给调用者,而不是生成器的创建者;

  8. 生成器被标记为正在运行;

  9. 生成器异常信息中的最后一个异常是从线程状态中的最后一个异常复制而来的;

  10. 线程状态异常信息设置为生成器异常信息的地址。这意味着如果调用者在生成器的执行周围进入断点,那么堆栈跟踪将通过生成器并清除违规代码;

  11. 生成器内部的帧在 Python/ceval.c 主执行循环中执行,并返回值;

  12. 线程状态最后异常信息重置为调用帧之前的值;

  13. 生成器被标记为未运行;

  14. 以下情况匹配返回值和调用生成器抛出的任何异常。请记住,生成器应该在耗尽时引发 StopIteration,无论是手动还是不产生值:

    • 如果没有从帧返回结果,则为生成器引发 StopIteration 并为异步生成器引发 StopAsyncIteration

    • 如果明确引发了 StopIteration,但这是协程或异步生成器,则引发 RuntimeError,因为这是不允许的;

    • 如果显式引发 StopAsyncIteration 并且这是一个异步生成器,则引发 RuntimeError,因为这是不允许的;

  15. 最后,将结果返回给 __next__() 的调用者。

将所有这些放在一起,你可以看到生成器表达式是如何成为一种强大的语法,其中单个关键字 yield 触发整个流程以创建一个唯一的对象,将编译后的 code 对象复制为一个属性,设置一个帧,然后在本地范围内存储变量列表。

Last updated