字节码解读generator运行机制

天波易谢,寸暑难留。

—— 唐·王勃

上一小节,我们重新考察了 生成器 ( 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_GENERATORCO_COROUTINECO_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 头文件,它的值是 0x20co_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源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: