Python的函數(shù)機制是很重要的部分,很多時候用python寫腳本,就是幾個函數(shù)簡單解決問題,不需要像java那樣必須弄個class什么的。
1 函數(shù)對象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是函數(shù)對象對應(yīng)的PyCodeObject,而func_globals則是函數(shù)的global名字空間,其實這個值是從上一層PyFrameObject傳遞而來。func_defaults是存儲函數(shù)默認(rèn)值的,后面分析函數(shù)參數(shù)的時候會提到,func_closure與閉包相關(guān),后面也會提到。
###func.py
def f():
print "Function"
f()
如上面例子func.py,該文件編譯后對應(yīng)2個PyCodeObject對象,一個是func.py本身,一個是函數(shù)f。而PyFunctionObject則是在執(zhí)行字節(jié)碼def f():時通過MAKE_FUNCTION指令生成。創(chuàng)建PyFunctionObject對象時,會將函數(shù)f對應(yīng)的PyCodeObject對象和當(dāng)前PyFrameObject對象傳入作為參數(shù),最終也就是賦值給PyFunctionObject中的func_code和func_globals字段了。在調(diào)用函數(shù)時,會將PyFunctionObject對象傳入到fast_function函數(shù)中,最終根據(jù)PyFunctionObject對象的func_code和func_globals字段構(gòu)建新的棧幀對象PyFrameObject,然后調(diào)用PyEval_EvalFrameEx在新的棧幀中執(zhí)行函數(shù)字節(jié)碼。其中PyEval_EvalFrameEx函數(shù)在之前的Python執(zhí)行原理中有提到過,當(dāng)時提到的PyEval_EvalCodeEx函數(shù)其實也是創(chuàng)建了新的棧幀對象PyFrameObject然后執(zhí)行PyEval_EvalFrameEx函數(shù)。
2 函數(shù)調(diào)用棧幀
函數(shù)調(diào)用通過棧幀來建立關(guān)聯(lián),每個被調(diào)用函數(shù)的棧幀PyFrameObject會通過f_back指針指向調(diào)用函數(shù)。而local,global以及builtin名字空間,local名字空間針對新的棧幀是全新的,而global名字空間則是由創(chuàng)建PyFrameObject時從PyFunctionObject傳遞過來。builtin名字空間則是共享調(diào)用者棧幀的(如果該棧幀是初始棧幀,則會先獲取builtin字典用于設(shè)置PyFrameObject的f_builtins字段)。
這里可以回顧一下C語言中的函數(shù)調(diào)用的棧幀關(guān)系。如下面的代碼,對應(yīng)的棧幀結(jié)構(gòu)如圖所示。在調(diào)用函數(shù)時,會先把函數(shù)參數(shù)會壓入當(dāng)前函數(shù)的棧幀中,每個函數(shù)都有自己的棧幀,由于esp會變化,所以其他函數(shù)會通過ebp來索引函數(shù)參數(shù)。

//函數(shù)調(diào)用棧幀測試代碼func.c
int 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中是如何來模擬函數(shù)參數(shù)傳遞的呢?從C語言函數(shù)調(diào)用過程可以知道,函數(shù)調(diào)用前,函數(shù)參數(shù)會先壓入到調(diào)用函數(shù)的棧幀中,而被調(diào)用函數(shù)則根據(jù)ebp來取參數(shù)。這里先回顧下PyFrameObject對象的結(jié)構(gòu),函數(shù)調(diào)用與PyFrameObject有著千絲萬縷的聯(lián)系。
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則是指向棧頂,在往運行時棧中壓入函數(shù)參數(shù)時,f_stacktop會變化,這兩個變量有點類似C里面的ebp和esp。f_localsplus則是指向局部變量+Cell對象+Free對象+運行時棧,其內(nèi)存布局如圖2所示。其中cell對象和free對象在閉包中用到,后面再看,這里主要說說局部變量和運行時棧。python在調(diào)用函數(shù)之前,會先將函數(shù)對象,函數(shù)參數(shù)壓入到當(dāng)前棧幀的運行時棧中,而在執(zhí)行函數(shù)時,會新建一個PyFrameObject棧幀對象,然后將函數(shù)參數(shù)拷貝到新棧幀的存儲局部變量的那塊空間中(也就是f_localsplus執(zhí)行的那塊內(nèi)存),接著才會調(diào)用PyEval_EvalFrameEx執(zhí)行被調(diào)用函數(shù)的代碼。

看下面的func2.py,通過這個例子可以來看一下函數(shù)調(diào)用流程。例子代碼和對應(yīng)字節(jié)碼如下。
#func2.py
def f(name, age):
age += 5
print '%s is %s old' % (name, age)
f('ssj', 18)
##字節(jié)碼
In [1]: import dis
In [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):跟之前說過的一樣,字節(jié)碼就是通過MAKE_FUNCTION指令創(chuàng)建PyFunctionObject對象并存儲到local名字空間,對應(yīng)的符號為函數(shù)名f,如果函數(shù)有默認(rèn)參數(shù),在MAKE_FUNCTION指令中還會設(shè)置默認(rèn)參數(shù)到func_defaults字段。而準(zhǔn)備調(diào)用函數(shù)時,則是先講函數(shù)對象和函數(shù)參數(shù)執(zhí)行函數(shù)時,會將函數(shù)參數(shù)壓棧,然后才通過CALL_FUNCTION指令調(diào)用函數(shù)。在調(diào)用PyEval_EvalFrameEx執(zhí)行函數(shù)代碼前,創(chuàng)建新的棧幀后,會先將函數(shù)參數(shù)拷貝到f_localsplus指向的那片局部變量空間中,然后才真正執(zhí)行函數(shù)f調(diào)用代碼。執(zhí)行函數(shù)f時,會將age參數(shù)壓入棧然后加上5,然后存儲到f_localsplus的第二個字段(第一個字段為name字符串"ssj")。函數(shù)參數(shù)位置變化如下圖所示。

3 函數(shù)執(zhí)行時名字空間
還是看第一節(jié)中給的例子func.py,其對應(yīng)的字節(jié)碼如下,其實定義函數(shù)def f():就是用函數(shù)對應(yīng)的PyCodeObject和棧幀對應(yīng)的f_globals構(gòu)建PyFunctionObject對象,然后通過STORE_NAME指令將PyFunctionObject對象與函數(shù)名f關(guān)聯(lián)并存儲到local名字空間。函數(shù)f對應(yīng)的PyCodeObject可以通過co.co_consts[0]獲取并查看。
In [1]: source = open('func.py').read()
In [2]: import dis
In [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名字空間,函數(shù)執(zhí)行時的名字空間略有不同。其global名字空間我們可以看到是通過PyFunctionObject從上一層棧幀傳遞來的,而local名字空間則是賦值為NULL,也就是函數(shù)中并沒有用到local名字空間,那么問題來了,函數(shù)中的那些局部變量是怎么訪問到的呢?那其實在函數(shù)中局部變量是通過LOAD_FAST指令(這個指令下一節(jié)會分析)來訪問的,也就是說它訪問的是f_localsplus的內(nèi)存空間,不需要動態(tài)查找f_locals這個PyDictObject,靜態(tài)的方法可以提供效率。
4 函數(shù)參數(shù)
Python中函數(shù)參數(shù)分為位置參數(shù),鍵參數(shù)以及擴展位置參數(shù)和擴展鍵參數(shù)。位置參數(shù)就是之前我們例子中的參數(shù),而鍵參數(shù)則是在調(diào)用函數(shù)指定參數(shù)的值。而擴展位置參數(shù)和擴展鍵參數(shù)格式則是類似*lst和**kwargs。位置參數(shù)還能設(shè)置默認(rèn)值,如果有默認(rèn)值,默認(rèn)值是在MAKE_FUNCTION指令賦值給func_defaults的。
下面的例子可以看到這幾種參數(shù)的用法。擴展位置參數(shù)在python內(nèi)部是通過一個元組對象存儲的,不管最終傳遞了幾個參數(shù)。而擴展鍵參數(shù)在python內(nèi)部則是通過一個字典對象存儲的。對于像 def f(a, b, *lst):這樣的函數(shù),如果調(diào)用函數(shù)時參數(shù)為f(1,2,3,4),其實在PyCodeObject對象中的co_argcount=2, co_nlocals=3。co_argcount是位置參數(shù)的個數(shù),而co_nlocals是局部變量數(shù)目,包括位置參數(shù)在內(nèi)。
##params1.py 位置參數(shù)和鍵參數(shù)
def f(a, b):
print a, b
f(1, 2)
f(b=2, a=1)
##params2.py 位置參數(shù),擴展位置參數(shù),擴展鍵參數(shù)
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 位置參數(shù)默認(rèn)值
def f(lst = []):
lst.append(3)
print lst
f() #打印[3]
f() #打印[3,3]
最后還要提到的一點的是,函數(shù)參數(shù)默認(rèn)值是在定義函數(shù)時設(shè)置的。如例子中的params3.py所示,如果指定了參數(shù)默認(rèn)值,而調(diào)用函數(shù)時又沒有覆蓋默認(rèn)值,則容易出現(xiàn)問題。要解決這個問題,可以在函數(shù)f中加個判斷if lst: lst = []。
5 閉包和裝飾器
之前提到過,PyCodeObject中有兩個字段與閉包相關(guān),分別是co_cellvars和co_freevars。其中co_cellvars通常是一個元組,里面保存的是嵌套作用域中使用的變量名集合,而co_freevars也通常是一個元組,里面保存的是外層作用域中的變量名集合。如下面這個閉包的例子,有三個PyCodeObject對象,closure.py本身,函數(shù)get_func以及inner_func分布對應(yīng)一個PyCodeObject。其中g(shù)et_func的PyCodeObject中的co_cellvars值是元組('value',),同時,inner_func的PyCodeObject的co_freevars存儲的內(nèi)容也是變量名value。
#closure.py 閉包
def get_func():
value = "inner"
def inner_func():
print value
return inner_func
show_value = get_func()
show_value()
可以看下get_func和inner_func的PyFrameObject的內(nèi)存布局,就可以大致了解閉包的機制了。其實就是在外層函數(shù)的局部變量中存儲內(nèi)層嵌套函數(shù)inner_func的PyFunctionObject對象,而PyFunctionObject中的func_closure字段是一個存儲PyCellObject的元組對象。在執(zhí)行inner_func時,會先將func_closure中存儲的PyCellObject對象拷貝到inner_func的PyFrameObject的free對象中,也就是cell對象后面那塊存儲空間,這樣在inner_func中通過freevars引用到value了(注意,這個freevars不是inner_func的PyCodeObject中的co_freevars,而是PyFrameObject中的對應(yīng)的內(nèi)存區(qū)域,雖然他們的內(nèi)容是一致的)。

裝飾器是基于閉包實現(xiàn)的,可以對一個函數(shù),方法,類進(jìn)行加工,實現(xiàn)一些額外功能,在實際編碼中會經(jīng)常用到,比如檢查用戶是否登錄,檢查輸入?yún)?shù)等,就可以用到裝飾器來減少冗余代碼。下面是一個裝飾器的例子:
#decorator.py 裝飾器
def wrapper(fn):
def _wrapper():
print 'wrapper '
fn()
return _wrapper
@wrapper
def func():
print 'real func'
if __name__ == "__main__":
func() #輸出'wrapper' 'real func'
更多裝飾器介紹,參見vamei的這篇文章 Python深入05 裝飾器
6 參考資料
- 《python源碼剖析》 主要例子和原理都是參照本書
- Python快速教程
- 宋勁松 《Linux C語言一站式編程》