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+b1 通過迭代方法
# 迭代測試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來實現該方面功能的文章。
新聞熱點
疑難解答