關於Python的super用法研究

來源:互聯網
上載者:User

原文地址: http://blog.csdn.net/johnsonguo/article/details/585193

一、問題的發現與提出

在Python類的方法(method)中,要調用父類的某個方法,在Python 2.2以前,通常的寫法如程式碼片段1:

程式碼片段1:

class A:    def __init__(self):        print "enter A"        print "leave A"class B(A):    def __init__(self):        print "enter B"        A.__init__(self) #顯示調用父類的初始化函數        print "leave B"

>>> b = B()

enter B

enter A

leave A

leave B

即,使用非綁定的類方法(用類名來引用的方法),並在參數列表中,引入待綁定的對象(self),從而達到調用父類的目的。

這樣做的缺點是,當一個子類的父類發生變化時(如類B的父類由A變為C時),必須遍曆整個類定義,把所有的通過非綁定的方法的類名全部替換過來,例如程式碼片段2,

程式碼片段2:

class B(C): # A --> C

def __init__(self):

print "enter B"

C.__init__(self) # A --> C

print "leave B"

如果代碼簡單,這樣的改動或許還可以接受。但如果代碼量龐大,這樣的修改可能是災難性的。

 

改成super的寫法

super使用樣本1:

class Base(object):    def __init__(self):        print("Base created")class ChildA(Base):    def __init__(self):        Base.__init__(self)class ChildB(Base):    def __init__(self):        super().__init__()print(ChildA(), ChildB())

協助文檔裡面給出的super使用的經典寫法

class C(B):    def method(self, arg):        super().method(arg)    # This does the same thing as:                               # super(C, self).method(arg)

 

因此,自Python 2.2開始,Python添加了一個關鍵字super,來解決這個問題。下面是Python 2.3的官方文檔說明:

super(type[, object-or-type])

Return the superclass of type. If the second argument is omitted the super object

returned is unbound. If the second argument is an object, isinstance(obj, type)

must be true. If the second argument is a type, issubclass(type2, type) must be

true. super() only works for new-style classes.

A typical use for calling a cooperative superclass method is:

class C(B):

def meth(self, arg):

super(C, self).meth(arg)

New in version 2.2.

從說明來看,可以把類B改寫如程式碼片段3:

程式碼片段3:

class A(object): # A must be new-style class

def __init__(self):

print "enter A"

print "leave A"

class B(C): # A --> C

def __init__(self):

print "enter B"

super(B, self).__init__()

print "leave B"

嘗試執行上面同樣的代碼,結果一致,但修改的代碼只有一處,把代碼的維護量降到最低,是一個不錯的用法。因此在我們的開發過程中,super關鍵字被大量使用,而且一直表現良好。

在我們的印象中,對於super(B, self).__init__()是這樣理解的:super(B, self)首先找到B的父類(就是類A),然後把類B的對象self轉換為類A的對象(通過某種方式,一直沒有考究是什麼方式,慚愧),然後“被轉換”的類A對象調用自己的__init__函數。考慮到super中只有指明子類的機制,因此,在多繼承的類定義中,通常我們保留使用類似程式碼片段1的方法。

有一天某同事設計了一個相對複雜的類體繫結構(我們先不要管這個類體系設計得是否合理,僅把這個例子作為一個題目來研究就好),代碼如程式碼片段4:

程式碼片段4:

class A(object):

def __init__(self):

print "enter A"

print "leave A"

class B(object):

def __init__(self):

print "enter B"

print "leave B"

class C(A):

def __init__(self):

print "enter C"

super(C, self).__init__()

print "leave C"

class D(A):

def __init__(self):

print "enter D"

super(D, self).__init__()

print "leave D"

class E(B, C):

def __init__(self):

print "enter E"

B.__init__(self)

C.__init__(self)

print "leave E"

class F(E, D):

def __init__(self):

print "enter F"

E.__init__(self)

D.__init__(self)

print "leave F"

>>> f = F()

enter F

enter E

enter B

leave B

enter C

enter D

enter A

leave A

leave D

leave C

leave E

enter D

enter A

leave A

leave D

leave F

明顯地,類A和類D的初始化函數被重複調用了2次,這並不是我們所期望的結果!我們所期望的結果是最多隻有類A的初始化函數被調用2次——其實這是多繼承的類體系必須面對的問題。我們把程式碼片段4的類體系畫出來,如:

object

| /

| A

| / |

B C D

/ / |

E |

/ |

F

按我們對super的理解,可以看出,在調用類C的初始化函數時,應該是調用類A的初始化函數,但事實上卻調用了類D的初始化函數。好一個詭異的問題!

二、走進Python的源碼世界

我們嘗試改寫程式碼片段4中的函數調用,但都沒有得到我們想要的結果,這不得不使我們開始懷疑:我們對super的理解是否出了問題。

我們重新閱讀了Python的官方文檔,正如您所見,官方文檔並沒有詳細的原理說明。到網路上去搜尋,確實有人發現了同樣的問題,並在一些論壇中討論,但似乎並沒有實質性的解答。既然,沒有前人的足跡,我們只好走進Python的源碼世界,去追溯問題的根源。

我們考查的是Python 2.3的源碼(估計Python 2.4的源碼可能也差不多)。首先,搜尋索引鍵"super"。唯一找到的是bltinmodule.c中的一句:

SETBUILTIN("super", &PySuper_Type);

於是,我們有了對super的第一個誤解:super並非是一個函數,而是一個類(PySuper_Type)。

在typeobject.c中找到了PySuper_Type的定義:

程式碼片段5:

PyTypeObject PySuper_Type = {

PyObject_HEAD_INIT(&PyType_Type)

0, /* ob_size */

"super", /* tp_name */

sizeof(superobject), /* tp_basicsize */

0, /* tp_itemsize */

/* methods */

super_dealloc, /* tp_dealloc */

0, /* tp_print */

0, /* tp_getattr */

0, /* tp_setattr */

0, /* tp_compare */

super_repr, /* tp_repr */

0, /* tp_as_number */

0, /* tp_as_sequence */

0, /* tp_as_mapping */

0, /* tp_hash */

0, /* tp_call */

0, /* tp_str */

super_getattro, /* tp_getattro */

0, /* tp_setattro */

0, /* tp_as_buffer */

Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |

Py_TPFLAGS_BASETYPE, /* tp_flags */

super_doc, /* tp_doc */

super_traverse, /* tp_traverse */

0, /* tp_clear */

0, /* tp_richcompare */

0, /* tp_weaklistoffset */

0, /* tp_iter */

0, /* tp_iternext */

0, /* tp_methods */

super_members, /* tp_members */

0, /* tp_getset */

0, /* tp_base */

0, /* tp_dict */

super_descr_get, /* tp_descr_get */

0, /* tp_descr_set */

0, /* tp_dictoffset */

super_init, /* tp_init */

PyType_GenericAlloc, /* tp_alloc */

PyType_GenericNew, /* tp_new */

PyObject_GC_Del, /* tp_free */

};

從程式碼片段5中可以得知,super類只改寫了幾個方法,最主要的包括:tp_dealloc,tp_getattro,tp_traverse,tp_init。

再看superobject的定義:

程式碼片段6:

typedef struct {

PyObject_HEAD

PyTypeObject *type;

PyObject *obj;

PyTypeObject *obj_type;

} superobject;

從程式碼片段6中可以看到superobject的資料成員僅有3個指標(3個對象的引用)。要知道這3個對象分別代表什麼,則必需考查super_init的定義:

程式碼片段7:

static int

super_init(PyObject *self, PyObject *args, PyObject *kwds)

{

superobject *su = (superobject *)self;

PyTypeObject *type;

PyObject *obj = NULL;

PyTypeObject *obj_type = NULL;

if (!PyArg_ParseTuple(args, "O!|O:super", &PyType_Type, &type, &obj))

return -1;

if (obj == Py_None)

obj = NULL;

if (obj != NULL) {

obj_type = supercheck(type, obj);

if (obj_type == NULL)

return -1;

Py_INCREF(obj);

}

Py_INCREF(type);

su->type = type;

su->obj = obj;

su->obj_type = obj_type;

return 0;

}

從代碼中可以看到,super_init首先通過PyArg_ParseTuple把傳入的參數列表解釋出來,分別放在type和obj變數之中。然後通過supercheck測試選擇性參數obj是否合法,並獲得執行個體obj的具體類類型。最後,把type, obj和obj_type記錄下來。也就是說,super對象只是簡單作了一些記錄,並沒有作任何轉換操作。

尋找問題的切入點是為什麼在類C中的super調用會切換到類D的初始化函數。於是在super_init中添加條件斷點,並跟蹤其後的Python代碼。最終進入到super_getattro函數——對應於super對象訪問名字__init__時的搜尋操作。

程式碼片段8(省略部分無關代碼,並加入一些注釋):

static PyObject *

super_getattro(PyObject *self, PyObject *name)

{

superobject *su = (superobject *)self;

int skip = su->obj_type == NULL;

……

if (!skip) {

PyObject *mro, *res, *tmp, *dict;

PyTypeObject *starttype;

descrgetfunc f;

int i, n;

starttype = su->obj_type; // 獲得搜尋的起點:super對象的obj_type

mro = starttype->tp_mro; // 獲得類的mro

……

for (i = 0; i < n; i++) { // 搜尋mro中,定位mro中的type

if ((PyObject *)(su->type) == PyTuple_GET_ITEM(mro, i))

break;

}

i++; // 切換到mro中的下一個類

res = NULL;

for (; i < n; i++) { // 在mro以後的各個命名空間中搜尋指定名字

tmp = PyTuple_GET_ITEM(mro, i);

if (PyType_Check(tmp))

dict = ((PyTypeObject *)tmp)->tp_dict;

else if (PyClass_Check(tmp))

dict = ((PyClassObject *)tmp)->cl_dict;

else

continue;

res = PyDict_GetItem(dict, name);

if (res != NULL) {

Py_INCREF(res);

f = res->ob_type->tp_descr_get;

if (f != NULL) {

tmp = f(res, su->obj,

(PyObject *)starttype);

Py_DECREF(res);

res = tmp;

}

return res;

}

}

}

return PyObject_GenericGetAttr(self, name);

}

從代碼中可以看出,super對象在搜尋命名空間時,其實是基於類執行個體的mro進行。那麼什麼是mro呢?尋找官方文檔,有:

PyObject* tp_mro

Tuple containing the expanded set of base types, starting with the type itself and

ending with object, in Method Resolution Order.

This field is not inherited; it is calculated fresh by PyType_Ready().

也就是說,mro中記錄了一個類的所有基類的類類型序列。查看mro的記錄,發覺包含7個元素,7個類名分別為:

F E B C D A object

從而說明了為什麼在C.__init__中使用super(C, self).__init__()會調用類D的初始化函數了。

我們把程式碼片段4改寫為:

程式碼片段9:

class A(object):

def __init__(self):

print "enter A"

super(A, self).__init__() # new

print "leave A"

class B(object):

def __init__(self):

print "enter B"

super(B, self).__init__() # new

print "leave B"

class C(A):

def __init__(self):

print "enter C"

super(C, self).__init__()

print "leave C"

class D(A):

def __init__(self):

print "enter D"

super(D, self).__init__()

print "leave D"

class E(B, C):

def __init__(self):

print "enter E"

super(E, self).__init__() # change

print "leave E"

class F(E, D):

def __init__(self):

print "enter F"

super(F, self).__init__() # change

print "leave F"

>>> f = F()

enter F

enter E

enter B

enter C

enter D

enter A

leave A

leave D

leave C

leave B

leave E

leave F

明顯地,F的初始化不僅完成了所有的父類的調用,而且保證了每一個父類的初始化函數只調用一次。

三、延續的討論

我們再重新看上面的類體系圖,如果把每一個類看作圖的一個節點,每一個從子類到父類的直接繼承關係看作一條有向邊,那麼該體系圖將變為一個有向圖。不能發現mro的順序正好是該有向圖的一個拓撲排序序列。

從而,我們得到了另一個結果——Python是如何去處理多繼承。支援多繼承的傳統的物件導向程式語言(如C++)是通過虛擬繼承的方式去實現多繼承中父類的建構函式被多次調用的問題,而Python則通過mro的方式去處理。

但這給我們一個難題:對於提供類體系的編寫者來說,他不知道使用者會怎麼使用他的類體系,也就是說,不正確的後續類,可能會導致原有類體系的錯誤,而且這樣的錯誤非常隱形,也難於發現。

 

四、小結

1. super並不是一個函數,是一個類名,形如super(B, self)事實上調用了super類的初始化函數,

產生了一個super對象;

2. super類的初始化函數並沒有做什麼特殊的操作,只是簡單記錄了類類型和具體執行個體;

3. super(B, self).func的調用並不是用於調用當前類的父類的func函數;

4. Python的多繼承類是通過mro的方式來保證各個父類的函數被逐一調用,而且保證每個父類函數

只調用一次(如果每個類都使用super);

5. 混用super類和非綁定的函數是一個危險行為,這可能導致應該調用的父類函數沒有調用或者一

個父類函數被調用多次。

 

註: 我感覺這種機制類似於物件導向編程裡的多態機制。這裡只是初始化函數這樣做,其他函數,應該也可以同樣用super()來調用。

 

附上 關於什麼是new style class的解釋

 

new-style class

Any class which inherits from object. This includes all built-in types like list and dict. Only new-style classes can use Python’s newer, versatile features like __slots__, descriptors, properties, and __getattribute__().

就是必須繼承自object對象的class. object對象是這樣的對象:

Return a new featureless object. object is a base for all new style classes. It has the methods that are common to all instances of new style classes.

object對象有一系列對所有執行個體化的new style class通用的方法。

相關文章

聯繫我們

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