Python源碼剖析筆記6-函數機制,python源碼剖析6-
Python的函數機制是很重要的部分,很多時候用python寫指令碼,就是幾個函數簡單解決問題,不需要像java那樣必須弄個class什麼的。
本文簡書地址:http://www.jianshu.com/p/d00108741a18
1 函數對象PyFunctionObject
PyFunctionObject對象的定義如下:
typedef struct { PyObject_HEAD PyObject *func_code; /* A code object */ PyObject *func_globals; /* A dictionary (other mappings won't do) */ PyObject *func_defaults; /* NULL or a tuple */ PyObject *func_closure; /* NULL or a tuple of cell objects */ PyObject *func_doc; /* The __doc__ attribute, can be anything */ PyObject *func_name; /* The __name__ attribute, a string object */ PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */ PyObject *func_weakreflist; /* List of weak references */ PyObject *func_module; /* The __module__ attribute, can be anything */} PyFunctionObject;
先說一下PyFunctionObject中幾個重要的變數,func_code, func_globals。其中func_code是函數對象對應的PyCodeObject,而func_globals則是函數的global名字空間,其實這個值是從上一層PyFrameObject傳遞而來。func_defaults是儲存函數預設值的,後面分析函數參數的時候會提到,func_closure與閉包相關,後面也會提到。
###func.pydef f(): print "Function"f()
如上面例子func.py,該檔案編譯後對應2個PyCodeObject對象,一個是func.py本身,一個是函數f。而PyFunctionObject則是在執行位元組碼def f():時通過MAKE_FUNCTION指令產生。建立PyFunctionObject對象時,會將函數f對應的PyCodeObject對象和當前PyFrameObject對象傳入作為參數,最終也就是賦值給PyFunctionObject中的func_code和func_globals欄位了。在調用函數時,會將PyFunctionObject對象傳入到fast_function函數中,最終根據PyFunctionObject對象的func_code和func_globals欄位構建新的棧幀對象PyFrameObject,然後調用PyEval_EvalFrameEx在新的棧幀中執行函數位元組碼。其中PyEval_EvalFrameEx函數在之前的Python執行原理中有提到過,當時提到的PyEval_EvalCodeEx函數其實也是建立了新的棧幀對象PyFrameObject然後執行PyEval_EvalFrameEx函數。
2 函數調用棧幀
函數調用通過棧幀來建立關聯,每個被調用函數的棧幀PyFrameObject會通過f_back指標指向調用函數。而local,global以及builtin名字空間,local名字空間針對新的棧幀是全新的,而global名字空間則是由建立PyFrameObject時從PyFunctionObject傳遞過來。builtin名字空間則是共用調用者棧幀的(如果該棧幀是初始棧幀,則會先擷取builtin字典用於設定PyFrameObject的f_builtins欄位)。
這裡可以回顧一下C語言中的函數調用的棧幀關係。如下面的代碼,對應的棧幀結構。在調用函數時,會先把函數參數會壓入當前函數的棧幀中,每個函數都有自己的棧幀,由於esp會變化,所以其他函數會通過ebp來索引函數參數。
//函數調用棧幀測試代碼func.cint bar(int c, int d){ int e = c + d; return e;}int foo(int a, int b){ return bar(a, b);}int main(void){ foo(2, 3); return 0;}
那麼python中是如何來類比函數參數傳遞的呢?從C語言函數調用過程可以知道,函數調用前,函數參數會先壓入到調用函數的棧幀中,而被調用函數則根據ebp來取參數。這裡先回顧下PyFrameObject對象的結構,函數調用與PyFrameObject有著千絲萬縷的聯絡。
typedef struct _frame { PyObject_VAR_HEAD struct _frame *f_back; /* previous frame, or NULL */ PyCodeObject *f_code; /* code segment */ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ PyObject *f_globals; /* global symbol table (PyDictObject) */ PyObject *f_locals; /* local symbol table (any mapping) */ PyObject **f_valuestack; /* points after the last local */ PyObject **f_stacktop; PyObject *f_trace; /* Trace function */ PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; PyThreadState *f_tstate; int f_lasti; /* Last instruction if called */ int f_lineno; /* Current line number */ int f_iblock; /* index in f_blockstack */ PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */} PyFrameObject;
PyFrameObject對象中,f_valuestack指向運行時棧的棧底,而f_stacktop則是指向棧頂,在往運行時棧中壓入函數參數時,f_stacktop會變化,這兩個變數有點類似C裡面的ebp和esp。f_localsplus則是指向局部變數+Cell對象+Free對象+運行時棧,其記憶體布局2所示。其中cell對象和free對象在閉包中用到,後面再看,這裡主要說說局部變數和運行時棧。python在調用函數之前,會先將函數對象,函數參數壓入到當前棧幀的運行時棧中,而在執行函數時,會建立一個PyFrameObject棧幀對象,然後將函數參數拷貝到新棧幀的儲存局部變數的那塊空間中(也就是f_localsplus執行的那塊記憶體),接著才會調用PyEval_EvalFrameEx執行被調用函數的代碼。
看下面的func2.py,通過這個例子可以來看一下函數調用流程。例子代碼和對應位元組碼如下。
#func2.pydef f(name, age): age += 5 print '%s is %s old' % (name, age)f('ssj', 18)##位元組碼In [1]: import disIn [2]: source = open('func2.py').read()In [3]: co = compile(source, 'func2.py', 'exec')In [4]: dis.dis(co) 1 0 LOAD_CONST 0 (<code object f at 0x10776faf8, file "func2.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 4 9 LOAD_NAME 0 (f) 12 LOAD_CONST 1 ('ssj') 15 LOAD_CONST 2 (18) 18 CALL_FUNCTION 2 21 POP_TOP 22 LOAD_CONST 3 (None) 25 RETURN_VALUE In [5]: dis.dis(co.co_consts[0]) 2 0 LOAD_FAST 1 (age) 3 LOAD_CONST 1 (5) 6 INPLACE_ADD 7 STORE_FAST 1 (age) ......
可以看到def f(name, age):跟之前說過的一樣,位元組碼就是通過MAKE_FUNCTION指令建立PyFunctionObject對象並儲存到local名字空間,對應的符號為函數名f,如果函數有預設參數,在MAKE_FUNCTION指令中還會設定預設參數到func_defaults欄位。而準備調用函數時,則是先講函數對象和函數參數執行函數時,會將函數參數壓棧,然後才通過CALL_FUNCTION指令調用函數。在調用PyEval_EvalFrameEx執行函數代碼前,建立新的棧幀後,會先將函數參數拷貝到f_localsplus指向的那片局部變數空間中,然後才真正執行函數f調用代碼。執行函數f時,會將age參數壓入棧然後加上5,然後儲存到f_localsplus的第二個欄位(第一個欄位為name字串”ssj”)。函數參數位置變化如所示。
3 函數執行時名字空間
還是看第一節中給的例子func.py,其對應的位元組碼如下,其實定義函數def f():就是用函數對應的PyCodeObject和棧幀對應的f_globals構建PyFunctionObject對象,然後通過STORE_NAME指令將PyFunctionObject對象與函數名f關聯並儲存到local名字空間。函數f對應的PyCodeObject可以通過co.co_consts[0]擷取並查看。
In [1]: source = open('func.py').read()In [2]: import disIn [3]: co = dis.dis(source, 'func.py', 'exec') 1 0 LOAD_CONST 0 (<code object f at 0x1107688a0, file "func.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 4 9 LOAD_NAME 0 (f) 12 CALL_FUNCTION 0 15 POP_TOP 16 LOAD_CONST 1 (None) 19 RETURN_VALUE In [10]: dis.dis(co.co_consts[0]) 2 0 LOAD_CONST 1 ('Function') 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
之前有說過python中分為local,global,builtin名字空間,函數執行時的名字空間略有不同。其global名字空間我們可以看到是通過PyFunctionObject從上一層棧幀傳遞來的,而local名字空間則是賦值為NULL,也就是函數中並沒有用到local名字空間,那麼問題來了,函數中的那些局部變數是怎麼訪問到的呢?那其實在函數中局部變數是通過LOAD_FAST指令(這個指令下一節會分析)來訪問的,也就是說它訪問的是f_localsplus的記憶體空間,不需要動態尋找f_locals這個PyDictObject,靜態方法可以提供效率。
4 函數參數
Python中函數參數分為位置參數,鍵參數以及擴充位置參數和擴充鍵參數。位置參數就是之前我們例子中的參數,而鍵參數則是在調用函數指定參數的值。而擴充位置參數和擴充鍵參數格式則是類似*lst和**kwargs。位置參數還能設定預設值,如果有預設值,預設值是在MAKE_FUNCTION指令賦值給func_defaults的。
下面的例子可以看到這幾種參數的用法。擴充位置參數在python內部是通過一個元組Object Storage Service的,不管最終傳遞了幾個參數。而擴充鍵參數在python內部則是通過一個字典Object Storage Service的。對於像 def f(a, b, *lst):這樣的函數,如果調用函數時參數為f(1,2,3,4),其實在PyCodeObject對象中的co_argcount=2, co_nlocals=3。co_argcount是位置參數的個數,而co_nlocals是局部變數數目,包括位置參數在內。
##params1.py 位置參數和鍵參數def f(a, b): print a, bf(1, 2)f(b=2, a=1)##params2.py 位置參數,擴充位置參數,擴充鍵參數def f(value, *lst, **kwargs): print value # -1 print lst # (1,2) print kwargs # {'a':3, 'b':4}f(-1, 1, 2, a=3, b=4)##params3.py 位置參數預設值def f(lst = []): lst.append(3) print lst f() #列印[3]f() #列印[3,3]
最後還要提到的一點的是,函數參數預設值是在定義函數時設定的。如例子中的params3.py所示,如果指定了參數預設值,而調用函數時又沒有覆蓋預設值,則容易出現問題。要解決這個問題,可以在函數f中加個判斷if lst: lst = []。
5 閉包和裝飾器
之前提到過,PyCodeObject中有兩個欄位與閉包相關,分別是co_cellvars和co_freevars。其中co_cellvars通常是一個元組,裡面儲存的是嵌套範圍中使用的變數名集合,而co_freevars也通常是一個元組,裡面儲存的是外層範圍中的變數名集合。如下面這個閉包的例子,有三個PyCodeObject對象,closure.py本身,函數get_func以及inner_func分布對應一個PyCodeObject。其中get_func的PyCodeObject中的co_cellvars值是元組(‘value’,),同時,inner_func的PyCodeObject的co_freevars儲存的內容也是變數名value。
#closure.py 閉包def get_func(): value = "inner" def inner_func(): print value return inner_funcshow_value = get_func()show_value()
可以看下get_func和inner_func的PyFrameObject的記憶體布局,就可以大致瞭解閉包的機制了。其實就是在外層函數的局部變數中儲存內層嵌套函數inner_func的PyFunctionObject對象,而PyFunctionObject中的func_closure欄位是一個儲存PyCellObject的元組對象。在執行inner_func時,會先將func_closure中儲存的PyCellObject對象拷貝到inner_func的PyFrameObject的free對象中,也就是cell對象後面那Block Storage空間,這樣在inner_func中通過freevars引用到value了(注意,這個freevars不是inner_func的PyCodeObject中的co_freevars,而是PyFrameObject中的對應的記憶體地區,雖然他們的內容是一致的)。
裝飾器是基於閉包實現的,可以對一個函數,方法,類進行加工,實現一些額外功能,在實際編碼中會經常用到,比如檢查使用者是否登入,檢查輸入參數等,就可以用到裝飾器來減少冗餘代碼。下面是一個裝飾器的例子:
#decorator.py 裝飾器def wrapper(fn): def _wrapper(): print 'wrapper ' fn() return _wrapper@wrapperdef func(): print 'real func'if __name__ == "__main__": func() #輸出'wrapper' 'real func'
更多裝飾器介紹,參見vamei的這篇文章 Python深入05 裝飾器
6 參考資料
- 《python源碼剖析》 主要例子和原理都是參照本書
- Python快速教程
- 宋勁松 《Linux C語言一站式編程》
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。