深入分析python yield

来源:互联网 发布:网络大电影男演员 编辑:程序博客网 时间:2024/05/20 23:06

一 概述

python中的yield是一个表达式,当函数中出现yield关键的时候,该函数会返回一个generator,可以通过迭代generator或者通过generator的send方法来激活generator执行,直到在有yield关键字的地方停下来。
generator是可迭代的,generator只能迭代一次,因为generator的数据是实时执行计算的。我们通过如下 斐波那契数列实现的例子来直观的了解下generator的基本使用方法。

def fib_gen(max):a, b = 0, 1for i in xrange(max):# send_value只是用来说明用法的测试send_value = yield bif send_value:print "send_value=%s" % send_valuea, b = b, a+b

1 通过迭代方法

# 迭代测试for fib_value in fib_gen(3):print fib_value# print result112
我们可以向迭代器一样的去迭代generator, 不过generator是顺序实时执行的,只能迭代一次。

2 通过send方法触发迭代器

# send方法的测试ge = fib_gen(2)# or ge.next()print ge.send(None)print ge.next()print ge.send("hello world")# print result11send_value=hello worldTraceback (most recent call last):  File "test.py", line 20, in <module>    print ge.send("hello world")StopIteration

(1) 当我们初次调用函数ge = fib_gen(2)的时候,函数还没有被执行,只是返回一个generator ge。可以通过send方法来触发generator的执行。
(2) 初次调用必须send(None), 此时函数fib_gen开始执行,执行到yield b时停下来,并返回b。
(3) 当我们继续调用ge.next(相当于ge.send(None)), ge会接着之前停下来的地方继续执行: send_value = yield b, 此时返回的send_value即为传递进去的参数None。继续执行send函数,从打印的结果可以看出,send_value="hello world"被传递到了函数中。
(4) 当ge执行到结束的时候,就会抛出StopIteration的异常,与其他的迭代器类似。
就这样,通过send将参数传递到函数中,函数通过yield的值作为send的返回值。

二 python虚拟机框架

要理解具体的yield的实现,首先要大概了解一下python虚拟机的执行流程。
python中虚拟机类似程序在x86机器上运行时栈的形式,以栈帧为基本单位,形成一个栈帧链,执行的时候在这些栈帧链中进行切换。在python中,一个模块、类以及函数的执行都会产生一个栈帧,然后执行这个栈帧。
python某个时刻执行的环境的栈帧链如下所示。




图1 Python执行的某个时候的运行环境

栈帧是通过一个PyFrameObject的结构实现,执行某个栈帧的时候,就是一个大的for循环,一条条读出code的字节码执行,串行的执行字节码指令。

三 yield的具体实现

在python中,yield是通过generator来实现,理解generator的具体实现,也就理解了yield的具体原理。

1 generator的结构

在python的源码中,generator的声明以及实现在genobject.h以及genobject.c中。先看一下generator的具体实现的结构。

// genobject.htypedef struct {    PyObject_HEAD    /* The gi_ prefix is intended to remind of generator-iterator. */     /* Note: gi_frame can be NULL if the generator is "finished" */    struct _frame *gi_frame;     /* True if generator is being executed. */    int gi_running;         /* The code object backing the generator */    PyObject *gi_code;     /* List of weak reference. */    PyObject *gi_weakreflist;} PyGenObject;

注释中基本上解释的比较清楚了,gi_frame就是指向前面介绍的栈帧的指针,generator的主要实现原理就是保存了当前的栈帧(栈帧中同样记录着当前执行到哪条字节码指令)。其他字段是一些辅助的信息,通过注释可以了解。

2 PyGen_New函数

PyGen_New为geobject提供唯一功能相关的对外接口,PyGen_New的具体实现如下。

// genobject.cPyObject *PyGen_New(PyFrameObject *f){    PyGenObject *gen = PyObject_GC_New(PyGenObject, &PyGen_Type);    if (gen == NULL) {        Py_DECREF(f);        return NULL;    }    gen->gi_frame = f;    Py_INCREF(f->f_code);    gen->gi_code = (PyObject *)(f->f_code);    gen->gi_running = 0;    gen->gi_weakreflist = NULL;    _PyObject_GC_TRACK(gen);    return (PyObject *)gen;}

PyGen_New接受一个PyFramObject 栈帧的指针,设置当前的gi_frame以及gi_code执行,保存当前的环境,返回一个generator,以及PyGenObject。

3 具体实现

(1) 函数调用以及包含yield的函数调用的实现
在python的实现中,每个栈帧是通过函数PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)来实现,函数接受一个PyFrameObject栈帧为参数,通过一个for循环,不断的读入字节码执行,通过一个巨大的switch语句,串行的执行字节码指令。

// ceval.c 代码有删减PyObject *PyEval_EvalFrameEx(PyFrameObject *f, int throwflag){    PyThreadState *tstate = PyThreadState_GET();    # 设置当前的frame    tstate->frame = f;    ...    for (;;)    {        switch (opcode)         {            case NOP:            ...            case LOAD_FAST:            ...            case CALL_FUNCTION:                PyObject **sp;                PCALL(PCALL_ALL);                sp = stack_pointer;                x = call_function(&sp, oparg);                stack_pointer = sp;                PUSH(x);                if (x != NULL)                    continue;                break;        }    }}

以菲波那切数列的实现为例,执行.py文件首先会产生一个PyFrameOject, 当执行到ge = fib_gen(2)的时候, 进行了一次函数调用,当前PyEval_EvalFrameEx函数执行到case CALL_FUNTION,进行一个call_funtion的函数调用,最后将返回结果压栈,继续执行下一条字节码指令。
我们看下call_function中具体做了什么,在call_funtion的调用中,最终会调用到函数PyEval_EvalCodeEx函数,从函数名字可以看出,这个函数的主要作用就是执行字节码,函数的部分实现如下。

// ceval.c 代码有删减或者修改PyObject *PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,           PyObject **args, int argcount, PyObject **kws, int kwcount,           PyObject **defs, int defcount, PyObject *closure){    register PyFrameObject *f;    register PyObject *retval = NULL;    PyThreadState *tstate = PyThreadState_GET();    f = PyFrame_New(tstate, co, globals, locals);    if (co->co_flags & CO_GENERATOR) {        /* Don't need to keep the reference to f_back, it will be set         * when the generator is resumed. */        Py_CLEAR(f->f_back);         PCALL(PCALL_GENERATOR);         /* Create a new generator that owns the ready to run frame         * and return that as the value. */        return PyGen_New(f);    }     retval = PyEval_EvalFrameEx(f,0);    return retval}

(1) 由函数的实现可以看出,在10行,通过code以及当前的环境变量,生成一个PyFrameObject的栈帧,然后执行该栈帧,将结果进行返回,这样就完成了一次函数的调用。
(2) 但是具有yield的函数返回的generator,具体的实现从第11行的分支开始,将当前frame的f_back清空,从栈帧链中移除,等到改frame具体执行的时候,再将其插入到python虚拟机执行的栈帧链中。
(3) 我们看到,此时并没有执行该frame,而是直接通过PyGen_New生成一个generator直接返回,这就是我们上面所说的,调用ge = fib_gen(2)其实返回一个generator,函数fib_gen并没有真正的开始执行。
(4) 那函数什么时候开始执行的呢,当我们通过迭代或者显示调用send的时候,该generator就开始执行起保存的frame,也是通过PyEval_EvalFrameEx函数来执行,如果再次遇到yield语句,如之前的流程一样,返回一个新的generator。

(2) generator的send函数
generator的send函数,激活genrator并执行,知道再次遇到yield返回一个新的generator或者直接执行结束。无论是迭代还是显示的调用next函数,最终都是通过generator的send函数来实现。
generator的send函数的具体实现如下:

// genobject.c 代码有删减或者修改static PyObject *gen_send_ex(PyGenObject *gen, PyObject *arg, int exc){    PyThreadState *tstate = PyThreadState_GET();    PyFrameObject *f = gen->gi_frame;    PyObject *result;     if (gen->gi_running) {        PyErr_SetString(PyExc_ValueError,                        "generator already executing");        return NULL;    }    ...    /* Generators always return to their most recent caller, not     * necessarily their creator. */    f->f_tstate = tstate;    Py_XINCREF(tstate->frame);    assert(f->f_back == NULL);    f->f_back = tstate->frame;     gen->gi_running = 1;    result = PyEval_EvalFrameEx(f, exc);    gen->gi_running = 0;     /* Don't keep the reference to f_back any longer than necessary.  It     * may keep a chain of frames alive or it could create a reference     * cycle. */    assert(f->f_back == tstate->frame);    Py_CLEAR(f->f_back);    /* Clear the borrowed reference to the thread state */    f->f_tstate = NULL;     return result;}

理解了上述yield函数调用相关原理,generator的send函数就很好理解了。
(1) 检查generator是否正在执行,如果不在执行,或者generator中的frame,并将该frame插入到当前python执行的栈帧链中,即f_back指向当前正在执行的frame。
(2) 设置当前generator的状态,并执行当前generator的frame, 清理一些引用,将结果进行返回。
(3) 当generator的frame执行完成后,可以接着打断的frame(即generator frame的f_back指向的frame)继续执行字节码指令。
(4) 如果在执行generator的frame中再次遇到yield关键字,则保存generator的frame(即当前正在执行的frame), 返回结果result为一个新的generator, 当调用该generator的send的时候,重复(1)~(4)

总结:

python的yield通过generator来实现,允许我们可以在函数执行过程中停下来,当调用send的时候继续执行。
我们可以利用python的yield来模拟类似协程方式的实现,利用yield,可以将一些异步的调用通过同步的写法来实现,后面会写一个利用yield来实现该方面功能的文章。














1 0
原创粉丝点击