# 生成器底层运行机制

# 生成器的创建

有如下生成器

def co_process(arg):
    print('task with argument {} started'.format(arg))
  
    data = yield 1
    print('step one finished, got {} from caller'.format(data))
  
    data = yield 2
    print('step two finished, got {} from caller'.format(data))
  
    data = yield 3
    print('step three finished, got {} from caller'.format(data))

我们知道co_process是一个特殊的函数对象,调用其结果并不会立刻执行函数体,而是得到一个生成器对象:

>>> co_process
<function co_process at 0x109768f80>
>>> genco = co_process('foo')
>>> genco
<generator object co_process at 0x109629450>
>>> genco.__class__
<class 'generator'>

尝试对co_process("foo")​进行反编译,发现其也是由统一的CALL_FUNCTION​函数调用字节码进行调用的,但是返回结果是一个生成器,应该是内部做了特殊处理:CALL_FUNCTION

>>> import dis
>>> dis.dis(compile("co_process('foo')", '', 'exec'))
  1           0 LOAD_NAME                0 (co_process)
              2 LOAD_CONST               0 ('foo')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

一路寻找,可以发现其最终调用的是_PyEval_EvalCodeWithName​函数。

_PyEval_EvalCodeWithName​函数先为目标函数 co_process​创建 栈帧 对象 f,然后检查代码对象标识。若代码对象带有 CO_GENERATOR​、CO_COROUTINE​或 CO_ASYNC_GENERATOR​标识,便创建生成器并返回:

    /* 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);
        }
        if (gen == NULL) {
            return NULL;
        }
      
        // 省略

        return gen;
    }

代码对象标识 co_flags 在编译时由语法规则确定,通过 co_process ,我们可以找到其代码对象标识:

>>> co_process.__code__.co_flags
99
>>> co_process.__code__.co_flags & 0x20
32

研究代码可以注意到,用于保存co_process​函数执行上下文的栈帧对象f,被作为一个重要字段保存在生成器对象gen中。

与普通函数一样,当 co_process 被调用时,Python 将为其创建栈帧对象,用于维护函数执行上下文 —— 代码对象全局名字空间局部名字空间 以及 运行栈 都在其中。

但不同的是,生成器的栈帧对象不会被接入调用链中,Python只会创建一个生成器对象并将其作为函数调用结果返回,在生成器对象的gi_frame字段中保存着真正的栈帧对象。

生成器对象底层由 PyGenObject 结构体表示,定义于 Include/genobject.h 头文件中。生成器类型对象同样由 PyTypeObject 结构体表示,全局只有一个,以全局变量的形式定义于 Objects/genobject.c 中,也就是 PyGen_Type 。

PyGenObject 结构体中的字段也很好理解,顾名即可思义,这也体现了变量名的作用:

  • ob_refcnt ,引用计数 ,这是任何对象都包含的公共字段;
  • ob_type ,对象类型 ,指向其类型对象,这也是任何对象都包含的公共字段;
  • gi_frame ,生成器执行时所需的 栈帧对象 ,用于保存执行上下文信息;
  • gi_running ,标识生成器是否运行中;
  • gi_code ,代码对象
  • gi_weakreflist ,弱引用相关,不深入讨论;
  • gi_name ,生成器名;
  • gi_qualname ,同上;
  • gi_exec_state ,生成器执行状态;

image

从Python中访问生成器对象genco可以进一步印证上述结论:

# 生成器创建后,尚未开始执行
>>> genco.gi_running
False

# 栈帧对象
>>> genco.gi_frame
<frame at 0x110601c90, file '<stdin>', line 1, code co_process>

# 生成器和栈帧的代码对象,均来自 co_process 函数对象
>>> genco.gi_code
<code object co_process at 0x11039c4b0, file "<stdin>", line 1>
>>> genco.gi_frame.f_code
<code object co_process at 0x11039c4b0, file "<stdin>", line 1>
>>> co_process.__code__
<code object co_process at 0x11039c4b0, file "<stdin>", line 1>

# 生成器的执行

co_process​函数被调用后,返回的是生成器genco​,函数体并未开始执行。

# 栈帧对象 f_lasti 字段记录当前字节码执行进度,-1 表示尚未开始执行
>>> genco.gi_frame.f_lasti
-1

我们了解可以使用next内建函数或者send方法启动生成器,并驱动其不断执行,尝试从这两个函数入手搜索生成器执行的密码。

next​函数定义于Python/bltinmodule.c​源文件-builtin_next。其关键之处只有一句,表明了其实质上是调用了生成器类型对象的tp_iternext​函数完成工作,即next(genco)​ 等价于 genco.__class__.__next__(genco)​:

res = (*it->ob_type->tp_iternext)(it); // 关键之处

`next(genco)`  等价于 `genco.__class__.__next__(genco)`

gen_iternext​函数定义于生成器类型对象PyGen_Type中-gen_iternext,其内部直接调用了gen_send_ex​函数-gen_send_ex。

另一方面,genco.send​也可以启动并驱动生成器的执行,根据 Objects/genobject.c 中的方法定义,它底层调用 _PyGen_Send 函数:

static PyMethodDef gen_methods[] = {
    {"send",(PyCFunction)_PyGen_Send, METH_O, send_doc},
    {"throw",(PyCFunction)gen_throw, METH_VARARGS, throw_doc},
    {"close",(PyCFunction)gen_close, METH_NOARGS, close_doc},
    {NULL, NULL}        /* Sentinel */
};

因此不管 gen_iternext 函数还是 _PyGen_Send 函数,都是直接调用 gen_send_ex 函数完成工作的,next和send的等价性也源于此:

image

gen_send_ex​函数中有两句关键的代码揭示了生成器的本质:

  • 首先第一行代码将生成器栈帧挂到当前调用链上
  • 然后第二行代码调用PyEval_EvalFrameEx​执行生成器栈帧
  • 生成器栈帧对象保存着生成器执行上下文,其中 f_lasti 字段跟踪生成器代码对象的执行进度。
    f->f_back = tstate->frame; // 关键

    result = PyEval_EvalFrameEx(f, exc); // 关键

image

# 生成器的暂停

我们了解生成器可以利用yield​语句将执行权归还给调用者,即生成器暂停执行的秘密就隐藏在yield​语句中,尝试对yield​语句进行编译:

def co_process(arg):
    print('task with argument {} started'.format(arg))
  
    data = yield 1
    ...
>>> import dis
>>> dis.dis(co_process)

// data = yield 1
...
  4          14 LOAD_CONST               2 (1)
             16 YIELD_VALUE
             18 STORE_FAST               1 (data)
...

LOAD_CONST​首先从常量列表中加载出需要带给调用者的值-yield右边的值,并将其加载的运行栈栈顶。

然后YIELD_VALUE字节码的处理过程中,首先从栈顶弹出yield值作为_PyEval_EvalFrameDefault​函数返回值,然后利用goto语句跳出for循环。

# YIELD_VALUE

        TARGET(YIELD_VALUE) {
            retval = POP();

            if (co->co_flags & CO_ASYNC_GENERATOR) {
                PyObject *w = _PyAsyncGenValueWrapperNew(retval);
                Py_DECREF(retval);
                if (w == NULL) {
                    retval = NULL;
                    goto error;
                }
                retval = w;
            }

            f->f_stacktop = stack_pointer;
            why = WHY_YIELD;
            goto fast_yield;
      

紧接着,_PyEval_EvalFrameDefault 函数将当前栈帧 (也就是生成器的栈帧) 从调用链中解开。注意到,yield 值被 _PyEval_EvalFrameDefault 函数返回,并最终被 send 方法或 next 函数返回给调用者。

image

# 生成器的恢复

当我们再次调用send方法时,生成器将恢复执行。我们直到,send方法被调用后,Python先把生成器栈帧对象挂到调用链,并最终调用PyEval_EvalFrameEx​函数逐条执行字节码。在这个过程中,send发送的数据会被放到生成器栈顶:

image

生成器执行进度被保存在 f_lasti 字段,生成器将从下一条字节码指令 STORE_FAST 继续执行。STORE_FAST 指令从栈顶取出 send 发来的数据,并保存到局部变量 data :

image

再接着,生成器将按照正常的逻辑,有条不紊地执行,直到遇到下一个 yield 语句或者生成器函数返回。

# 总结

  • 生成器函数编译后代码对象带有 CO_GENERATOR 标识;
  • 如果函数代码对象带 CO_GENERATOR 标识,被调用时 Python 将创建生成器对象;
  • 生成器创建的同时,Python 还创建一个栈帧对象,用于维护代码对象执行上下文;
  • 调用 next/send 驱动生成器执行,Python 将生成器栈帧对象接入调用链,开始执行字节码;
  • 执行到 yield 语句时,Python 将 yield 右边的值放入栈顶,并结束字节码执行循环,执行权回到上一个栈帧;
  • yield 值最终作为 next 函数或 send 方法的返回值,被调用者取得;
  • 再次调用 next/send ,Python 重新将生成器栈帧对象接入调用链恢复执行,通过 send 发送的值被放在栈顶;
  • 生成器函数重新启动后,从 YIELD_VALUE 后的字节码恢复执行,可从栈顶获得调用者发来的值;
  • 代码执行权就这样在调用者和生成器间来回切换,而生成器栈顶被用来传值;