深入分析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某个时刻执行的环境的栈帧链如下所示。
栈帧是通过一个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来实现该方面功能的文章。
- 深入分析python yield
- Python 深入理解yield
- Python 深入理解yield
- Python 深入理解yield
- Python 深入理解yield
- Python 深入理解yield
- python---深入理解yield
- Python yield分析
- 深入理解python中的yield
- python中yield深入理解
- 深入理解python的yield和generator
- Python 中yield的原理分析
- 深入应用python关键字yield--实现任务调度
- python----yield
- Python yield
- python yield
- python yield
- python --yield
- 【Codeforces 756 D. Artsem and Saunders】+ 思维 + 构造
- AngularJs服务-$log
- 找不到scalameter 利用intellij scala 导入jar 添加resource
- 数据结构与算法分析笔记与总结(java实现)--二叉树14:把二叉树打印成多行
- 关于WindowManager.LayoutParams.TYPE_SYSTEM_ALERT适配的问题
- 深入分析python yield
- 用ajax实现文件下载
- getifaddrs导致Segmentation fault
- 数据结构与算法分析笔记与总结(java实现)--二叉树15:对称的二叉树
- jforum用到MySQL设置字符集为UTF8(Windows版)解决中文乱码
- 实现一键下载安装
- POJ1009_Edge Detection_跳跃式编码
- 即时定位与地图构建(SLAM)与基于视觉的SLAM(VSLAM)
- 递归的应用(二)