# 生成器底层运行机制
# 生成器的创建
有如下生成器
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 ,生成器执行状态;
从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的等价性也源于此:
在gen_send_ex函数中有两句关键的代码揭示了生成器的本质:
- 首先第一行代码将生成器栈帧挂到当前调用链上
- 然后第二行代码调用
PyEval_EvalFrameEx执行生成器栈帧 - 生成器栈帧对象保存着生成器执行上下文,其中 f_lasti 字段跟踪生成器代码对象的执行进度。
f->f_back = tstate->frame; // 关键
result = PyEval_EvalFrameEx(f, exc); // 关键
# 生成器的暂停
我们了解生成器可以利用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_VALUETARGET(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 函数返回给调用者。
# 生成器的恢复
当我们再次调用send方法时,生成器将恢复执行。我们直到,send方法被调用后,Python先把生成器栈帧对象挂到调用链,并最终调用PyEval_EvalFrameEx函数逐条执行字节码。在这个过程中,send发送的数据会被放到生成器栈顶:
生成器执行进度被保存在 f_lasti 字段,生成器将从下一条字节码指令 STORE_FAST 继续执行。STORE_FAST 指令从栈顶取出 send 发来的数据,并保存到局部变量 data :
再接着,生成器将按照正常的逻辑,有条不紊地执行,直到遇到下一个 yield 语句或者生成器函数返回。
# 总结
- 生成器函数编译后代码对象带有 CO_GENERATOR 标识;
- 如果函数代码对象带 CO_GENERATOR 标识,被调用时 Python 将创建生成器对象;
- 生成器创建的同时,Python 还创建一个栈帧对象,用于维护代码对象执行上下文;
- 调用 next/send 驱动生成器执行,Python 将生成器栈帧对象接入调用链,开始执行字节码;
- 执行到 yield 语句时,Python 将 yield 右边的值放入栈顶,并结束字节码执行循环,执行权回到上一个栈帧;
- yield 值最终作为 next 函数或 send 方法的返回值,被调用者取得;
- 再次调用 next/send ,Python 重新将生成器栈帧对象接入调用链恢复执行,通过 send 发送的值被放在栈顶;
- 生成器函数重新启动后,从 YIELD_VALUE 后的字节码恢复执行,可从栈顶获得调用者发来的值;
- 代码执行权就这样在调用者和生成器间来回切换,而生成器栈顶被用来传值;