Python源碼剖析筆記6-函數機制,python源碼剖析6-

來源:互聯網
上載者:User

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語言一站式編程》

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.