標籤:運算 one defaults 完整 比較 python2 lca 虛擬機器 src
函數中局部變數的訪問
在完成了對函數參數的剖析後,我們再來看看,在Python中,函數的局部變數時如何?的。前面提到過,函數參數也是一種局部變數。所以,其實局部變數的實現機制與函數參數的實現機制是完全一樣的。這個“一樣”是什麼意思呢?
之前我們剖析過Python虛擬機器的一些指令,如果要訪問一個變數,應該使用LOAD_NAME指令,應該依照local、global、builtin這三個名字空間裡去檢索變數名所對應的變數值。然後在調用函數時,Python虛擬機器通過PyFrame_New建立新的PyFrameObject對象時,那個至關重要的local對象並沒有建立
在對Python虛擬機器機制分析的過程中,我們得知當直接調用一個指令碼時,指令碼對應的PyCodeObject對象中f_locals和f_globals實際上是同一個對象。那麼在函數執行時,對變數的讀寫決不能反應在f_globals上,否則,一個函數執行完,函數外部不就可以知道函數的局部變數嗎?所以,函數是如何對局部變數進行讀寫呢?我們來對一個帶有局部變數的函數用dis模組進行位元組碼指令的解釋
>>> def f(a, b):... c = a + b... print(c)... >>> import dis>>> dis.dis(f) 2 0 LOAD_FAST 0 (a) 3 LOAD_FAST 1 (b) 6 BINARY_ADD 7 STORE_FAST 2 (c) 3 10 LOAD_FAST 2 (c) 13 PRINT_ITEM 14 PRINT_NEWLINE 15 LOAD_CONST 0 (None) 18 RETURN_VALUE
這裡,我們並未發現有LOAD_NAME或者STORE_NAME這樣對名字空間的讀寫指令,取而代之的LOAD_FAST和STORE_FAST,這是不是印證我們之前所說的,局部變數的實現機制與函數參數的實現機制是完全一樣的,局部變數的讀寫也是在f_localsplus上
為什麼函數的實現中沒有使用local名字空間呢?這是因為函數中的局部變數總是固定不變的,所以在編譯時間就能確定局部變數使用的記憶體空間的位置,也能確定訪問局部變數的位元組碼指令應該如何訪問記憶體。有了這些資訊,Python就能使用靜態方法來實現局部變數,而不需要藉助於動態地尋找PyDictObject對象的技術。畢竟,函數調用實在太普遍了,靜態方法可以極大地提高函數的執行效率
嵌套函數
在Python中,有一個核心的概念叫名字空間,一段代碼的執行的結果不光取決於代碼中的符號,更多地是取決於代碼中符號的語義,而這個運行時語義正是由名字空間所決定的。名字空間是在運行時由Python虛擬機器動態維護的,但有時,我們希望名字空間靜態化。換句話說,我們希望代碼不受名字空間變化的影響,始終保持一致的行為和結果。這樣做有什麼意義呢?
假如我們想要定一個基準值,然後將許多值與這個值進行比較,最簡單的方法就是寫一個函數:
>>> def compare(base, value):... return value > base... >>> compare(10, 5)False>>> compare(10, 20)True
我們以10作為基準值,與5和20進行比較,但是會發現,每次調用函數時,都必須多傳一個10。於是,Python提供了嵌套函數
>>> base = 1>>> def get_compare(base):... def real_compare(value):... return value > base... return real_compare... >>> compare_with_10 = get_compare(10)>>> compare_with_10(5)False>>> compare_with_10(20)True
如上述代碼,我們只設定了一次基準值。此後,在每次進行比較操作時,儘管調用的實際函數real_compare的local名字空間並沒有base,而get_compare函數之外的global名字空間中有"base = 1",但是函數調用的結果顯示,real_compare以我們傳入的10作為base,而不是以get_compare函數之外的base作為基準書
也就是說,在real_compare這個函數作為傳回值被傳遞給compare_with_10的時候,有一個名字空間已經與real_compare緊緊地綁定在一起,在執行real_compare的代碼時,這個名字空間又恢複了,這就是將名字空間靜態化的方法。這個名字空間與函數捆綁後的結果被稱為一個閉包。在前面我們看到,PyFunctionObject是Python虛擬機器專門為包裹位元組碼指令、global名字空間、預設參數值準備的大包袱,都能在PyFunctionObject中找到其位置,同樣,Python中的閉包也是通過PyFunctionObject對象來實現
實現閉包的基石
我們先來看看PyCodeObject、PyFunctionObject、PyFrameObject這些我們熟悉的對象中,與閉包有關的屬性。閉包的建立通常是利用嵌套函數來完成,在PyCodeObject中,與嵌套函數相關的屬性是co_freevars、co_cellvars。兩者具體含義如下:
- co_freevars:通常是一個tupple,儲存嵌套的範圍中使用的變數名集合
- co_cellvars:通常是一個tupple,儲存使用了的外層範圍中的變數名集合
考慮下面的代碼:
# cat demo4.py def get_func(): a = 1 value = "inner" def inner_func(): print(value) return inner_func
很顯然,上述的代碼會編譯出3個PyCodeObject對象,其中有兩個,一個與函數get_func對應,一個與函數inner_func對應。那麼,與get_func對應的PyCodeObject對象中co_cellvars就應該包含字串"value",因為其嵌套範圍(inner_func的範圍)中使用了這個符號。同理,與函數inner_func對應的PyCodeObject對象中的co_freevars中應該也有字串"value"。下面,我們來證實一下:
>>> source = open("demo4.py").read()>>> co = compile(source, "demo4.py", "exec")>>> co.co_consts(<code object get_func at 0x7efc8c8c1b70, file "demo4.py", line 1>, None)
demo4.py對應的PyCodeObject中,co_consts這個元組第一個對象就是get_func對應的PyCodeObject,我們將其取出,然後看一下其中的co_cellvars
>>> get_func_co = co.co_consts[0]>>> get_func_co.co_name‘get_func‘>>> get_func_co.co_cellvars(‘value‘,)
從前面的demo4.py可以知道,儘管get_func中除去變數value,還有一個變數a,但是a沒有函數內部的嵌套函數使用,所以co_cellvars只有符號value,沒有符號a。我們都知道,PyCodeObject是可以嵌套PyCodeObject的,既然get_func的PyCodeObject對象被我們取出,不妨,我們再取出inner_func對應的PyCodeObject,但在這之前,我們要先看一下,inner_func的PyCodeObject,到底處於get_func對應的PyCodeObject的co_consts哪個位置
>>> get_func_co.co_consts(None, 1, ‘inner‘, <code object inner_func at 0x7efc8c8c1cd8, file "demo4.py", line 5>)>>> inner_func_co = get_func_co.co_consts[3]>>> inner_func_co.co_freevars(‘value‘,)
可以看到,inner_func對應的PyCodeObject中,co_freevars果然有符號value
以上,便是PyCodeObject中與閉包相關的屬性。下面,我們再來看看PyFrameObject對象中,與閉包屬性相關的對象。其實在這裡,只有一個對象和閉包相關,就是我們的老朋友f_localsplus
在PyFrame_New函數中,有這樣一段代碼:
ncells = PyTuple_GET_SIZE(code->co_cellvars);nfrees = PyTuple_GET_SIZE(code->co_freevars);extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
extras正是f_localsplus指向的那片記憶體的大小,這片記憶體是屬於運行時棧、局部變數、cell對象(co_cellvars)、和free對象(co_freevars)。下面,展現一下f_localsplus的布局:
圖1-1 f_localsplus的完整記憶體布局
閉包的實現
在介紹完閉包的基石後,我們就可以開始追蹤閉包的具體實現過程了。但是我們好像還忘了一件事,之前說過與閉包有關的對象,除了PyCodeObject、PyFrameObject,還有一個PyFunctionObject。沒關係,我們很快就會瞭解到PyFunctionObject與閉包那些不能不說的事。不過首先,我們還是要來看一下demo5.py編譯後的位元組碼指令:
其實,demo5.py相比demo4.py,僅僅是把get_func函數中的變數a給移除
# cat demo5.pydef get_func(): value = "inner" def inner_func(): print(value) return inner_funcshow_value = get_func()show_value()
有了demo5.py,我們就可以逐層分析PyCodeObject對象,閉包指令是長什麼樣的
首先,我們先得到demo5.py對應的PyCodeObject對象
>>> source = open("demo5.py").read()>>> co = compile(source, "demo5.py", "exec")>>> import dis>>> dis.dis(co) 1 0 LOAD_CONST 0 (<code object get_func at 0x255d120, file "demo5.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (get_func) 10 9 LOAD_NAME 0 (get_func) 12 CALL_FUNCTION 0 15 STORE_NAME 1 (show_value) 11 18 LOAD_NAME 1 (show_value) 21 CALL_FUNCTION 0 24 POP_TOP 25 LOAD_CONST 1 (None) 28 RETURN_VALUE
demo5.py的指令序列我們已經很熟悉了,重點不是在這,而是在get_func和inner_func的指令序列
get_func指令序列
>>> co.co_consts(<code object get_func at 0x255d120, file "demo5.py", line 1>, None)>>> get_func_co = co.co_consts[0]>>> get_func_co.co_flags3>>> dis.dis(get_func_co) 2 0 LOAD_CONST 1 (‘inner‘) 3 STORE_DEREF 0 (value) 4 6 LOAD_CLOSURE 0 (value) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (inner_func) 7 21 LOAD_FAST 0 (inner_func) 24 RETURN_VALUE
inner_func指令序列
>>> get_func_co.co_consts(None, ‘inner‘, <code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>)>>> inner_func_co = get_func_co.co_consts[2]>>> inner_func_co.co_flags19>>> dis.dis(inner_func_co) 5 0 LOAD_DEREF 0 (value) 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
在fast_function中,我們先前有介紹過一個快速通道,但這個快速通道不是任何函數都能進的,在進之前要滿足若干條件,其中一個條件就是co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE),即co_flags為67。可惜get_func和inner_func的co_flags都不是67,註定只能乖乖走PyEval_EvalCodeEx
如果當前PyCodeObject的co_cellvars的長度不為0,將進入下面代碼的分支,Python虛擬機器會如同處理預設參數一樣,將co_cellvars中的東西拷貝到新建立的PyFrameObject的f_localsplus中
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){……if (PyTuple_GET_SIZE(co->co_cellvars)){int i, j, nargs, found;char *cellname, *argname;PyObject *c;nargs = co->co_argcount;if (co->co_flags & CO_VARARGS)nargs++;if (co->co_flags & CO_VARKEYWORDS)nargs++;for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i){//[1]:獲得被嵌套函數共用的符號名cellname = PyString_AS_STRING(PyTuple_GET_ITEM(co->co_cellvars, i));found = 0;for (j = 0; j < nargs; j++){argname = PyString_AS_STRING(PyTuple_GET_ITEM(co->co_varnames, j));if (strcmp(cellname, argname) == 0){c = PyCell_New(GETLOCAL(j));if (c == NULL)goto fail;GETLOCAL(co->co_nlocals + i) = c;found = 1;break;}}//處理被嵌套函數共用外層函數的預設參數if (found == 0){c = PyCell_New(NULL);if (c == NULL)goto fail;SETLOCAL(co->co_nlocals + i, c);}}}……}
在上述代碼的[1]處,Python虛擬機器獲得了被內層嵌套函數引用的符號名,在我們的例子中,就是獲得了一個字串"value"。這裡的found是被內層嵌套函數引用的符號是否已經與某個值綁定的標識,或者說與某個對象建立了約束關係。只有在內層嵌套函數引用的是外層函數的一個有預設值的參數時,這個標識才可能為1。對於我們的例子,found一定為0。因為get_func所對應的PyCodeObject中,co_varnames尋找不到符號"value"。所以Python虛擬機器接下來會建立cell對象——PyCellObject
cellobject.c
typedef struct {PyObject_HEADPyObject *ob_ref;/* Content of the cell or NULL when empty */} PyCellObject;
這個對象非常簡單,僅僅維護一個ob_ref,指向一個PyObject對象,我們來看看PyCellObject的建立代碼
cellobject.c
PyObject *PyCell_New(PyObject *obj){PyCellObject *op;op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);if (op == NULL)return NULL;op->ob_ref = obj;Py_XINCREF(obj);_PyObject_GC_TRACK(op);return (PyObject *)op;}
在我們的例子中,建立的PyCellObject對象維護的ob_ref指向了NULL,也就是說,現在還不知道符號value到底是什麼東西,那麼什麼時候才能知道呢?在value = "inner"這個指派陳述式執行的時候。隨後,這個cell對象被拷貝到新建立的PyFrameObject對象的f_localsplus中。值的注意的是,這個對象被拷貝到的位置是co_co_nlocals + i。說明在n_localsplus中,cell對象的位置是在局部變數之後的,這完全符合圖1-1所示的記憶體布局
在處理co_cellvars時,有一個奇怪的地方,在我們建立PyCellObject對象的過程中,代碼[1]處的cellname完全忽略了。實際上,這和前面分析到的Python函數機制對局部變數符號的訪問方式從對dict的尋找變為對list的索引是一個道理。在get_func函數執行的過程中,對value這個cell變數的訪問將通過基於索引訪問f_localsplus完成,因為完全不需要再知道cellname了。這個cellname實際上是在處理內層嵌套函數引用外層函數的預設參數時產生的
在處理了cell對象之後,Python虛擬機器將進入PyEval_EvalFrameEx,從而正式開始對函數get_func的調用過程,這裡,我們再貼一下get_func的位元組碼指令序列
>>> dis.dis(get_func_co) 2 0 LOAD_CONST 1 (‘inner‘) 3 STORE_DEREF 0 (value) 4 6 LOAD_CLOSURE 0 (value) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (inner_func) 7 21 LOAD_FAST 0 (inner_func) 24 RETURN_VALUE
首先執行"0 LOAD_CONST 1"指令將PyStringObject對象"inner"壓入到運行時棧,然後Python虛擬機器開始執行一條對我們是全新的位元組碼指令——STORE_DEREF
ceval.c
PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag){……freevars = f->f_localsplus + co->co_nlocals;……}
ceval.c
case STORE_DEREF:w = POP();x = freevars[oparg];PyCell_Set(x, w);Py_DECREF(w);continue;
從運行時棧彈出的是PyStringObject對象"inner",而從f_localsplus中取得的是PyCellObject對象。原來,STORE_DEREF是要設定PyCellObject對象中的ob_ref
cellobject.c
#define PyCell_SET(op, v) (((PyCellObject *)(op))->ob_ref = v)int PyCell_Set(PyObject *op, PyObject *obj){if (!PyCell_Check(op)) {PyErr_BadInternalCall();return -1;}Py_XDECREF(((PyCellObject*)op)->ob_ref);Py_XINCREF(obj);PyCell_SET(op, obj);return 0;}
這樣一來,f_localsplus就發生了變化,1-2
圖1-2 設定cell對象之後的get_func函數的PyFrameObject對象
現在在get_func的環境中我們知道了value符號對應著一個PyStringObject對象,但是閉包的作用是將這個約束進行凍結,使得在嵌套函數inner_func被調用時還能使用這個約束。這一次,又需要用到PyFunctionObject這個對象了。在執行demo5.py中def inner_func()運算式時,Python虛擬機器就將(value, "inner")這個約束塞到PyFunctionObject中
ceval.c
case LOAD_CLOSURE:x = freevars[oparg];Py_INCREF(x);PUSH(x);if (x != NULL)continue;break;
"6 LOAD_CLOSURE 0"指令將剛剛放置好的PyCellObject對象取出,並壓入運行時棧,接著執行"9 BUILD_TUPLE 1"指令將PyCellObject對象打包進一個tupple中,顯然,這個tupple可以放置多個PyCellObject對象。不過,我們的例子只有一個PyCellObject對象
隨後,Python虛擬機器通過執行"12 LOAD_CONST 2"指令將inner_func對應的PyCodeObject對象也壓入到運行時棧,接著以一個"15 MAKE_CLOSURE 0"指令完成約束與PyCodeObject的綁定
ceval.c
case MAKE_CLOSURE:{v = POP(); //獲得PyCodeObject對象x = PyFunction_New(v, f->f_globals);//綁定global名字空間Py_DECREF(v);if (x != NULL){v = POP();//獲得tupple,其中包含PyCellObject對象的集合err = PyFunction_SetClosure(x, v);//綁定約束集合Py_DECREF(v);}//處理擁有預設值的參數if (x != NULL && oparg > 0){v = PyTuple_New(oparg);if (v == NULL){Py_DECREF(x);x = NULL;break;}while (--oparg >= 0){w = POP();PyTuple_SET_ITEM(v, oparg, w);}err = PyFunction_SetDefaults(x, v);Py_DECREF(v);}PUSH(x);break;}
運算式"def inner_func()"所對應的最後一條"18 STORE_FAST 0"指令將所建立的PyFunctionObject對象放置到了f_localsplus中。這樣,f_localsplus又發生了變化
圖1-3 設定function對象之後的get_func函數中的PyFrameObject對象
在get_func的最後,這個建立的PyFunctionObject對象將作為傳回值返回給上一個棧幀,並被壓入到該棧幀的運行時棧中
使用閉包(closure)
閉包是在get_func中建立的,而對於閉包的使用,則是在inner_func中。在執行"show_value()"對應的CALL_FUNCTION時,和inner_func對應的PyCodeObject中的co_flags裡包含了CO_NESTED,所以在fast_function中依舊不能通過快速通道的驗證,還是要進入到PyEval_EvalCodeEx
這裡,我們再看一下inner_func對應的位元組碼指令序列
>>> dis.dis(inner_func_co) 5 0 LOAD_DEREF 0 (value) 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
我們已經看到,inner_func對應的PyCodeObject這種co_freevars裡有引用外部範圍的符號名,在PyEval_EvalCodeEx中,就會對這個co_freevars進行處理
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){……if (PyTuple_GET_SIZE(co->co_freevars)){int i;for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i){PyObject *o = PyTuple_GET_ITEM(closure, i);Py_INCREF(o);freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;}}……}
其中,closure變數作為最後一個函數參數傳遞進來,我們看看在fast_function中到底傳進來什麼
//funcobject.c#define PyFunction_GET_CLOSURE(func) (((PyFunctionObject *)func) -> func_closure)//ceval.cstatic PyObject *fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk){……return PyEval_EvalCodeEx(co, globals, (PyObject *)NULL, (*pp_stack) - n, na, (*pp_stack) - 2 * nk, nk, d, nd, PyFunction_GET_CLOSURE(func));}
原來傳進來的就是在PyFunctionObject對象中與PyCodeObject對象綁定的裝滿了PyCellObject對象的tupple,所以在PyEval_EvalCodeEx中,進行的動作就是將tupple中一個個PyCellObject對象放入到f_localsplus中相應的位置。處理完closure後,inner_func對應的PyFrameObject中的f_localsplus1-4所示
圖1-4 設定cell對象之後的inner_func函數的PyFrameObject對象
所以,在inner_func調用過程中,當引用到外層範圍的符號時,一定是到f_localsplus中的free變數地區中獲得符號對應的值。這正是inner_func函數中"print(value)"運算式對應的第一條位元組碼指令"0 LOAD_DEREF 0"
ceval.c
case LOAD_DEREF:x = freevars[oparg];//獲得PyCellObject對象w = PyCell_Get(x);//獲得PyCellObject.ob_obj指向的對象if (w != NULL){PUSH(w);continue;}……
裝飾器(Decorator)
在closure技術的基礎上,Python實現的裝飾器(Decorator),來看下面的例子:
# cat demo6.py def should_say(fn): def say(*args): print("say something...") fn(*args) return say@should_saydef func(): print("in func")func()# python2.5 demo6.py say something...in func
實際上,我們可以完全不用decorator,而實現同樣的效果,只需要對demo6.py做小小的修改
# cat demo7.py def should_say(fn): def say(*args): print("say something...") fn(*args) return saydef func(): print("in func")func = should_say(func)func()# python2.5 demo7.py say something...in func
會發現demo6.py和demo7.py的輸出結果相同。實際上,基於上面對closure的剖析,裝飾器的行為就很好理解了,裝飾器只是用一個函數來封裝另一個函數,類似"func = should_say(func)"的形式。現在,我們來看看demo6.py和demo7.py中部分編譯結果
demo6.py位元組碼指令序列
@should_saydef func(): //位元組碼指令序列9 LOAD_NAME 0 (should_say)12 LOAD_CONST 1 (<code object func at 0x255d3f0, file "demo6.py", line 9>)15 MAKE_FUNCTION 018 CALL_FUNCTION 121 STORE_NAME 1 (func) print("in func")
demo7.py位元組碼指令序列
def func()://位元組碼指令序列9 LOAD_CONST 1 (<code object func at 0x255d558, file "demo7.py", line 9>)12 MAKE_FUNCTION 015 STORE_NAME 1 (func) print("in func")func = should_say(func)//位元組碼指令序列18 LOAD_NAME 0 (should_say)21 LOAD_NAME 1 (func)24 CALL_FUNCTION 127 STORE_NAME 1 (func)
在demo7.py中,"15 STORE_NAME 1"和"21 LOAD_NAME 1"這兩條位元組碼指令互為逆運算,可以刪除。如此一來,demo7.py編譯後的位元組碼指令序列和demo6.py編譯後的位元組碼指令序列,除了"LOAD_NAME 0"的位置不同外,其餘的都完全相同。
Python虛擬機器函數機制之閉包和裝飾器(七)