上一小节,我们重新考察了 生成器 ( generator )的运行时行为,发现了它神秘的一面。生成器函数体代码可以通过 yield 关键字暂停和恢复执行,这个特性可以用来实现 协程 。
那么,生成器诸多神奇特性是如何实现的呢?为了更好地理解生成器协程,我们有必要深入研究它的运行机制。
生成器的创建
我们对上节中的 co_process 生成器略加修改后作为研究对象,继续深入字节码,力求洞察生成器执行的原理:
1
2
3
4
5
6
7
8
9
10
11
|
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 one finished, got {} from caller'.format(data))
data = yield 3
print('step one finished, got {} from caller'.format(data))
|
co_process 是一个特殊的函数对象,它被调用后并不会立刻执行函数体,而是得到一个生成器对象:
1
2
3
4
5
6
7
|
>>> co_process
<function co_process at 0x109768f80>
>>> genco = co_process('foo')
>>> genco
<generator object co_process at 0x109629450>
>>> genco.__class__
<class 'generator'>
|
在函数机制部分,我们知道函数调用由 CALL_FUNCTION 字节码负责:
1
2
3
4
5
6
7
8
|
>>> 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
|
那么,什么情况下函数调用会返回生成器呢?顺着 CALL_FUNCTION 字节码处理逻辑,不难找到答案。
CALL_FUNCTION 字节码在 Python/ceval.c 中处理,它主要是调用 call_function 函数完成工作。call_function 函数根据被调用对象类型区别处理,可分为 类方法 、 函数对象 , 普通可调用对象 等等。
在这个例子中,被调用对象是函数对象。因此,call_function 函数调用位于 Objects/call.c 中的 _PyFunction_FastCallKeywords 函数,而它则进一步调用位于 Python/ceval.c 的 _PyEval_EvalCodeWithName 函数。
_PyEval_EvalCodeWithName 函数先为目标函数 co_process 创建 栈帧 对象 f,然后检查代码对象标识。若代码对象带有 CO_GENERATOR 、CO_COROUTINE 或 CO_ASYNC_GENERATOR 标识,便创建生成器并返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/* 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 ,我们可以找到其代码对象标识:
1
2
|
>>> co_process.__code__.co_flags
99
|
CO_GENERATOR 宏定义于 Include/code.h 头文件,它的值是 0x20 ,co_process 代码对象确实带有该标识:
1
2
|
>>> co_process.__code__.co_flags & 0x20
32
|
注意到,用于保存 co_process 函数执行上下文的栈帧对象 f ,作为一个重要字段保存于生成器对象 gen 中:
至此,生成器对象的创建过程已经浮出水面。与普通函数一样,当 *co_process_ 被调用时,_Python_ 将为其创建栈帧对象,用于维护函数执行上下文—— **代码对象** 、 **全局名字空间** 、 **局部名字空间** 以及 **运行栈** 都在其中。
与普通函数不同的是,co_process 代码对象带有生成器标识。Python 不会立即执行代码对象,栈帧对象也不会被接入调用链,因此 f_back 字段是空的。相反,Python 创建了一个生成器对象,并将其作为函数调用结果返回。
生成器对象底层由 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 ,进一步印证我们在源码中得到的结论:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 生成器创建后,尚未开始执行
>>> 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>
|
那么,生成器的执行、暂停和恢复又是什么样的实现机制呢?点击“阅读原文”,获取更多详情!
【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: