函式與方法的區別
隨著我們越來越頻繁使用Python, 我們難免會接觸到類, 接觸到類屬性和方法.但是很多新手包括我, 不知道方法 和 函式 的區別,這次簡單來討論下, 如果有哪裡認識不正確, 希望大神提點指教!
先來看兩個定義吧:
function(函式) —— A series of statements which returns some value toa caller. It can also be passed zero or more arguments which may beused in the execution of the body.
method(方法) —— A function which is defined inside a class body. Ifcalled as an attribute of an instance of that class, the methodwill get the instance object as its first argument (which isusually called self).
從上面可以看出, 別的程式語言一樣, Function也是包含一個函式頭和一個函式體, 也同樣支援0到n個形參,而Method則是在function的基礎上, 多了一層類的關係, 正因為這一層類, 所以區分了 function 和 method.而這個過程是通過 PyMethod_New實現的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
PyObject * PyMethod_New(PyObject *func, PyObject *self, PyObject *klass) { register PyMethodObject *im; // 定義方法結構體 im = free_list; if (im != NULL) { free_list = (PyMethodObject *)(im->im_self); PyObject_INIT(im, &PyMethod_Type); // 初始化 numfree--; } else { im = PyObject_GC_New(PyMethodObject, &PyMethod_Type); if (im == NULL) return NULL; } im->im_weakreflist = NULL; Py_INCREF(func); /* 往下開始通過 func 配置 method*/ im->im_func = func; Py_XINCREF(self); im->im_self = self; Py_XINCREF(klass); im->im_class = klass; _PyObject_GC_TRACK(im); return (PyObject *)im; |
所以本質上, 函式和方法的區別是: 函式是屬於 FunctionObject, 而 方法是屬 PyMethodObject
簡單來看下程式碼:
1 2 3 4 5 6 7 8 9 10 |
def aa(d, na=None, *kasd, **kassd): pass class A(object): def f(self): return 1 a = A() print '#### 各自方法描述 ####' print '## 函式 %s' % aa print '## 類方法 %s' % A.f print '## 例項方法 %s' % a.f |
輸出結果:
1 2 3 4 |
#### 各自方法描述 #### ## 函式 <function aa at 0x000000000262AB38> ## 類方法 <unbound method A.f> ## 例項方法 <bound method A.f of <__main__.A object at 0x0000000002633198>> |
Bound Method 和 Unbound Method
method 還能再分為 Bound Method 和 Unbound Method, 他們的差別是什麼呢? 差別就是 Bound method 多了一個例項繫結的過程!
A.f 是 unbound method, 而 a.f 是 bound method, 從而驗證了上面的描述是正確的!
看到這, 我們應該會有個問題:
1 |
方法的繫結, 是什麼時候發生的? 又是怎樣的發生的? |
帶著這個問題, 我們繼續探討.很明顯, 方法的繫結, 肯定是伴隨著class的例項化而發生,我們都知道, 在class裡定義方法, 需要顯示傳入self引數, 因為這個self是代表即將被例項化的物件。
我們需要dis模組來協助我們去觀察這個繫結的過程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
[root@iZ23pynfq19Z ~]# cat 33.py class A(object): def f(self): return 123 a = A() print A.f() print a.f() ## 命令執行 ## [root@iZ23pynfq19Z ~]# python -m dis 33.py 1 0 LOAD_CONST 0 ('A') 3 LOAD_NAME 0 (object) 6 BUILD_TUPLE 1 9 LOAD_CONST 1 (<code object A at 0x7fc32f0b5030, file "33.py", line 1>) 12 MAKE_FUNCTION 0 15 CALL_FUNCTION 0 18 BUILD_CLASS 19 STORE_NAME 1 (A) 4 22 LOAD_NAME 1 (A) 25 CALL_FUNCTION 0 28 STORE_NAME 2 (a) 5 31 LOAD_NAME 1 (A) 34 LOAD_ATTR 3 (f) 37 CALL_FUNCTION 0 40 PRINT_ITEM 41 PRINT_NEWLINE 6 42 LOAD_NAME 2 (a) 45 LOAD_ATTR 3 (f) 48 CALL_FUNCTION 0 51 PRINT_ITEM 52 PRINT_NEWLINE 53 LOAD_CONST 2 (None) 56 RETURN_VALUE |
dis輸出說明: 第一列是程式碼的函式, 第二列是指令的偏移量, 第三列是視覺化指令, 第四列是引數, 第五列是指令根據引數計算或者查詢的結果
我們們可以看到 第4列 和第五列, 分別就是對應: print A.f() 和 print a.f()
他們都是同樣的位元組碼, 都是從所在的codeobject中的co_name取出引數對應的名字, 正因為引數的不同, 所以它們分別取到 A 和 a,下面我們需要來看看 LOAD_ATTR 的作用是什麼:
1 2 3 4 5 6 7 8 9 10 11 |
//取自: python2.7/objects/ceval.c TARGET(LOAD_ATTR) { w = GETITEM(names, oparg); // 從co_name 取出 f v = TOP(); // 將剛才壓入棧的 A/a 取出來 x = PyObject_GetAttr(v, w); // 取得真正的執行函式 Py_DECREF(v); SET_TOP(x); if (x != NULL) DISPATCH(); break; } |
通過 SET_TOP, 已經將我們需要真正執行的函式壓入執行時棧, 接下來就是通過 CALL_FUNCTION 來呼叫這個函式物件, 繼續來看看具體過程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//取自: python2.7/objects/ceval.c TARGET(CALL_FUNCTION) { PyObject **sp; PCALL(PCALL_ALL); sp = stack_pointer; #ifdef WITH_TSC x = call_function(&sp, oparg, &intr0, &intr1); #else x = call_function(&sp, oparg); // 細節請往下看 #endif stack_pointer = sp; PUSH(x); if (x != NULL) DISPATCH(); break; } static PyObject * call_function(PyObject ***pp_stack, int oparg) { int na = oparg & 0xff; // 位置引數個數 int nk = (oparg>>8) & 0xff; // 關鍵位置引數的個數 int n = na + 2 * nk; // 總的個數和 PyObject **pfunc = (*pp_stack) - n - 1; // 當前棧位置-引數個數,得到函式物件 PyObject *func = *pfunc; PyObject *x, *w; ... // 省略前面細節, 只看關鍵呼叫 if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) { /* optimize access to bound methods */ PyObject *self = PyMethod_GET_SELF(func); PCALL(PCALL_METHOD); PCALL(PCALL_BOUND_METHOD); Py_INCREF(self); func = PyMethod_GET_FUNCTION(func); Py_INCREF(func); Py_SETREF(*pfunc, self); na++; n++; } else Py_INCREF(func); READ_TIMESTAMP(*pintr0); if (PyFunction_Check(func)) x = fast_function(func, pp_stack, n, na, nk); else x = do_call(func, pp_stack, na, nk); READ_TIMESTAMP(*pintr1); Py_DECREF(func); } |
我們們來捋下呼叫順序:
1 |
CALL_FUNCTION -> call_function -> 根據函式的型別 -> 執行對應的操作 |
當程式執行到call_function時, 主要有的函式型別判斷有: PyCFunction, PyMethod, PyFunction
在這裡, 虛擬機器已經判斷出func是不屬於PyCFunction, 所以將會落入上面原始碼的判斷分支中, 而它將要做的,就是分別通過 PyMethod_GET_SELF, PyMethod_GET_FUNCTION 獲得self物件和func函式, 然後通過呼叫 Py_SETREF(*pfunc, self):
1 2 3 4 5 6 7 |
// Py_SETREF 定義如下 #define Py_SETREF(op, op2) do { PyObject *_py_tmp = (PyObject *)(op); (op) = (op2); Py_DECREF(_py_tmp); } while (0) |
可以看出, Py_SETREF是用這個self物件替換了pfunc指向的物件了, 而pfunc在上面已經提及到了, 就是當時壓入執行時棧的函式物件. 除了這幾步, 還有更重要的就是, na 和 n 都分別自增1
看回上面的 a.f(), 我們們可以知道, 它是不需要引數的, 所以理論上 na,nk和n都是0, 但是因為f是method(方法), 經過上面一系列操作, 它將會傳入一個self,而na也會變成1, 又因為*pfunc已經被替換成self, 相應程式碼:
1 2 3 4 |
if (PyFunction_Check(func)) x = fast_function(func, pp_stack, n, na, nk); else x = do_call(func, pp_stack, na, nk); |
所以它不再進入function的尋常路了, 而是走do_call, 然後就開始真正的呼叫;
其實這個涉及到Python呼叫函式的整個過程, 因為比較複雜, 後期找個時間專門談談這個
聊到這裡, 我們已經大致清楚, 一個method(方法) 在呼叫時所發生的過程.明白了函式和方法的本質區別, 那麼回到主題上 來說下 Unbound 和 Bound, 其實這兩者差別也不大. 從上面我們得知, 一個方法的建立, 是需要self, 而呼叫時, 也會使用self,而只有例項化物件, 才有這個self, class是沒有的, 所以像下面的執行, 是失敗的額
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class A(object): def f(self): return 1 a = A() print '#### 各自方法等效呼叫 ####' print '## 類方法 %s' % A.f() print '## 例項方法 %s' % a.f() ## 輸出結果 ## #### 各自方法等效呼叫 #### Traceback (most recent call last): File "C:/Users/Administrator/ZGZN_Admin/ZGZN_Admin/1.py", line 20, in <module> print '## 類方法 %s' % A.f() TypeError: unbound method f() must be called with A instance as first argument (got nothing instead) |
錯誤已經很明顯了: 函式未繫結, 必須要將A的例項作為第一個引數
既然它要求第一個引數是 A的例項物件, 那我們就試下修改程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class A(object): def f(self): return 1 a = A() print '#### 各自方法等效呼叫 ####' print '## 類方法 %s' % A.f(a) #傳入A的例項a print '## 例項方法 %s' % a.f() ## 結果 ## #### 各自方法等效呼叫 #### ## 類方法 1 ## 例項方法 1 |
可以看出來, Bound 和 Unbound判斷的依據就是, 當方法真正執行時, 有沒有傳入例項, A.f(a) 和 a.f() 用法的區別只是在於, 第一種需要人為傳入例項才能呼叫, 而第二種, 是虛擬機器幫我們做好了傳入例項的動作, 不用我們那麼麻煩而已, 兩種方法本質上是等價的。
請使用手機”掃一掃”x