Python虛擬機器函數機制之閉包和裝飾器(七)

來源:互聯網
上載者:User

標籤:運算   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虛擬機器函數機制之閉包和裝飾器(七)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.