Python: 函數與方法的區別 以及 Bound Method 和 Unbound Method

LucieSettle 7年前發布 | 22K 次閱讀 Python Python開發

函數與方法的區別

隨著我們越來越頻繁使用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的基礎上, 多了一層類的關系, 正因為這一層類, 所以區分了 functionmethod .而這個過程是通過 PyMethod_New 實現的

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

簡單來看下代碼:

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

輸出結果:

#### 各自方法描述 ####

函數   <function aa at 0x000000000262AB38>

類方法   <unbound method A.f>

實例方法 <bound method A.f of <main.A object at 0x0000000002633198>>

</code></pre>

Bound Method 和 Unbound Method

method還能再分為 Bound MethodUnbound Method , 他們的差別是什么呢? 差別就是 Bound method 多了一個 實例綁定 的過程!

A.f是 unbound method , 而 a.fbound method , 從而驗證了上面的描述是正確的!

看到這, 我們應該會有個問題:

 方法的綁定, 是什么時候發生的? 又是怎樣的發生的?

帶著這個問題, 我們繼續探討.很明顯, 方法的綁定, 肯定是伴隨著 class 的實例化而發生,我們都知道, 在 class 里定義方法, 需要顯示傳入 self 參數, 因為這個 self 是代表即將被實例化的對象。

我們需要 dis 模塊來協助我們去觀察這個綁定的過程:

[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 </code></pre>

dis輸出說明: 第一列是代碼的函數, 第二列是指令的偏移量, 第三列是可視化指令, 第四列是參數, 第五列是指令根據參數計算或者查找的結果

咱們可以看到 第4列 和第五列, 分別就是對應: print A.f() 和 print a.f()

他們都是同樣的字節碼, 都是從所在的codeobject中的 co_name 取出參數對應的名字, 正因為參數的不同, 所以它們分別取到 A 和 a,下面我們需要來看看 LOAD_ATTR 的作用是什么:

//取自: 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 來調用這個函數對象, 繼續來看看具體過程:

//取自: 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); } </code></pre>

咱們來捋下調用順序:

CALL_FUNCTION -> call_function -> 根據函數的類型 -> 執行對應的操作

當程序運行到 call_function 時, 主要有的函數類型判斷有: PyCFunction, PyMethod, PyFunction

在這里, 虛擬機已經判斷出func是不屬于 PyCFunction , 所以將會落入上面源碼的判斷分支中, 而它將要做的,就是分別通過 PyMethod_GET_SELF, PyMethod_GET_FUNCTION 獲得self對象和func函數, 然后通過調用 Py_SETREF (*pfunc, self):

// Py_SETREF 定義如下

define Py_SETREF(op, op2)                      

    do {                                                 PyObject _py_tmp = (PyObject )(op);           (op) = (op2);                                   Py_DECREF(_py_tmp);                         } while (0) </code></pre>

可以看出, Py_SETREF 是用這個self對象替換了 pfunc指向的對象了, 而 pfunc在上面已經提及到了, 就是當時壓入運行時棧的函數對象. 除了這幾步, 還有更重要的就是, na 和 n 都分別自增1

看回上面的 a.f(), 咱們可以知道, 它是不需要參數的, 所以理論上 na,nk和n都是0, 但是因為f是method(方法), 經過上面一系列操作, 它將會傳入一個self,而na也會變成1, 又因為*pfunc已經被替換成self, 相應代碼:

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(方法) 在調用時所發生的過程.明白了函數和方法的本質區別, 那么回到主題上 來說下 UnboundBound , 其實這兩者差別也不大. 從上面我們得知, 一個方法的創建, 是需要self, 而調用時, 也會使用self,而只有實例化對象, 才有這個self, class是沒有的, 所以像下面的執行, 是失敗的額

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) </code></pre>

錯誤已經很明顯了: 函數未綁定, 必須要將A的實例作為第一個參數

既然它要求第一個參數是 A的實例對象, 那我們就試下修改代碼:

class A(object):
    def f(self):
        return 1
a = A()
 
print '#### 各自方法等效調用 ####'
print '## 類方法 %s' % A.f(a)   #傳入A的實例a
print '## 實例方法 %s' % a.f()
 

結果

各自方法等效調用

類方法 1

實例方法 1

</code></pre>

可以看出來, BoundUnbound 判斷的依據就是, 當方法真正執行時, 有沒有傳入實例, A.f(a) 和 a.f() 用法的區別只是在于, 第一種需要人為傳入實例才能調用, 而第二種, 是虛擬機幫我們做好了傳入實例的動作, 不用我們那么麻煩而已, 兩種方法 本質上是等價的。

 

來自:http://python.jobbole.com/87698/

 

 本文由用戶 LucieSettle 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!