深入分析python yield

一 概述

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

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

1 通过迭代方法

# 迭代测试
for fib_value in fib_gen(3):
	print fib_value

# print result
1
1
2
我们可以向迭代器一样的去迭代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 result
1
1
send_value=hello world
Traceback (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.h
typedef 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.c
PyObject *
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来实现该方面功能的文章。














  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值