摘要
本文講述Python繼承關(guān)系中如何通過super()調(diào)用“父類”方法,super(Type, CurrentClass)返回CurrentClassMROType的下一個(gè)類的代理;以及如何設(shè)計(jì)Python類以便正確初始化。

1. 單繼承中父類方法調(diào)用

在繼承中,調(diào)用父類方法是很有必要的。調(diào)用父類方法的場景有很多:

  • 比如必須調(diào)用父類的構(gòu)造方法__init__才能正確初始化父類實(shí)例屬性,使得子類實(shí)例對象能夠繼承到父類實(shí)例對象的實(shí)例屬性;

  • 再如需要重寫父類方法時(shí),有時(shí)候沒有必要完全摒棄父類實(shí)現(xiàn),只是在父類實(shí)現(xiàn)前后加一些實(shí)現(xiàn),最終還是要調(diào)用父類方法

單繼承是最簡單的繼承關(guān)系,多繼承過于復(fù)雜,而且使用起來容易出錯(cuò)。因此一些高級(jí)語言完全摒棄了多繼承,只支持單繼承;一些高級(jí)語言雖然支持多繼承,但也不推薦使用多繼承。Python也是一樣,在不能完全掌握多繼承時(shí),最好不好使用,單繼承能滿足絕大部分的需求。

1.1 非綁定方式調(diào)用

綁定方法與非綁定方法的區(qū)別與聯(lián)系參見:Python基礎(chǔ)-類

如有以下繼承關(guān)系兩個(gè)類:

class D(object):
    def test(self):
        print('test in D')class C(D):
    def test(self):
        print('test in C')

現(xiàn)在要求在子類Ctest函數(shù)中調(diào)用父類Dtest實(shí)現(xiàn)。我們能想到最直接的方法恐怕是直接引用類對象D的函數(shù)成員test了:

class D(object):
    def test(self):
        print('test in D')class C(D):
    def test(self):
        print('test in C')
        D.test(self)

嘗試測試一下:

c = C()
c.test()

output:

test in C
test in D

看來非綁定的方式確實(shí)滿足了當(dāng)前調(diào)用父類方法的需求。

1.2 builtin 函數(shù) super

參考Python tutorial關(guān)于super的描述: super(\[type\[, object-or-type\]\])

Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class. The search order is same as that used by getattr() except that the type itself is skipped.

super函數(shù)返回委托類type的父類或者兄弟類方法調(diào)用的代理對象。super用來調(diào)用已經(jīng)在子類中重寫了的父類方法。方法的搜索順序與getattr()函數(shù)相同,只是參數(shù)類type本身被忽略。

1.3 綁定方式調(diào)用

使用綁定方式調(diào)用父類方法,自然不能顯式傳入?yún)?shù)當(dāng)前對象(self)。現(xiàn)在super函數(shù)能夠返回對父類的代理,因?yàn)樵趩卫^承中子類有且僅有一個(gè)父類,所以父類是明確的,我們完全清楚調(diào)用的父類方法是哪個(gè):

class D(object):
    def test(self):
        print('test in D')class C(D):
    def test(self):
        print('test in C')        super().test() # super(C, self).test()的省略形式

2. 深入super

事實(shí)上,super函數(shù)返回的代理對象是一個(gè)bultin class super,正如它的名字所指,類super代理了子類的父類。在單繼承關(guān)系中,super代理的類很容易找到嗎,就是子類的唯一父類;但是在多繼承關(guān)系中,super除了能代理子類的父類外,還有可能代理子類的兄弟類。

2.1 復(fù)雜的多繼承

在多繼承關(guān)系中,繼承關(guān)系可能會(huì)相當(dāng)復(fù)雜。

class D(object):
    
    def test(self):
        print('test in D')class C(D):
    
    def test(self):
        print('test in C')class B(D):
    
    def test(self):
        print('test in B')class A(B, C):
    pass

A繼承層次結(jié)構(gòu)如下:

  object
    |    D
   / \  B   C
   \ /    A

A的繼承關(guān)系中存在菱形結(jié)構(gòu),即可以通過多條路徑從類A到達(dá)某個(gè)父類,這里是D。

如果現(xiàn)在要求在類A中調(diào)用“父類”的test方法,需要一種對test方法的搜索解析順序,來決定到底是調(diào)用B,C或Dtest方法。

2.2 方法解析順序(MRO)

上面提出的對test的方法的搜索順序,就是方法解析順序了。

深度優(yōu)先
Python舊式類中,方法解析順序是深度優(yōu)先,多個(gè)父類從左到右。
廣度優(yōu)先
Python新式類中,方法解析順序是廣度優(yōu)先,多個(gè)父類從左到右。

所以上面的解析順序是:A -> B -> C -> D -> object

Python中,類的__mro__屬性展示了方法搜索順序,可以調(diào)用mro()方法或者直接引用__mro__得到搜索順序:

print(A.mro())print(A.__mro__)

output:

[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>](<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)

所以

a = A()
a.test() # output: test in B

變化的MRO
即使是同一個(gè)類,在不同的MRO中位置的前后關(guān)系都是不同的。如以下類:

class D(object):
    
    def test(self):
        print('test in D')class C(D):
    
    def test(self):
        print('test in C')class B(D):
    
    def test(self):
        print('test in B')

B的繼承層次結(jié)構(gòu)為:

  object
    |    D
   / \  C   B

B的MRO:B -> D -> object
對比類A的MRO:A -> B -> C -> D -> object
同樣的類B,在兩個(gè)不同的MRO中位置關(guān)系也是不同的。可以說,在已有的繼承關(guān)系中加入新的子類,會(huì)在MRO中引入新的類,并且改變解析順序。

那么可以想象,同樣在類B的test中通過super調(diào)用父類方法,在不同的MRO中實(shí)際調(diào)用的方法是不同的。

如下:

class D(object):
    
    def test(self):
        print('test in D')class C(D):
    
    def test(self):
        print('test in C')        super().test()class B(D):
    
    def test(self):
        print('test in B')        super().test()class A(B, C):
    passb = B()
b.test()print('==========')
a = A()
a.test()

output:

test in B
test in D==========test in B
test in C
test in D

因?yàn)樵谠械念愱P(guān)系中加入BC的子類A,使得在Btest方法中調(diào)用supertest方法發(fā)生了改變,原來調(diào)用的是其父類Dtest方法,現(xiàn)在調(diào)用的是其兄弟類Ctest方法。
從這里可以看出super不總是代理子類的父類,還有可能代理其兄弟類。

因此在設(shè)計(jì)多繼承關(guān)系的類體系時(shí),要特別注意這一點(diǎn)。

2.3 再看super方法

方法super([type[, object-or-type]]),返回的是對type的父類或兄弟類的代理。

  • 如果第二個(gè)參數(shù)省略,返回的super對象是未綁定到確定的MRO上的:

  • 如果第二個(gè)參數(shù)是對象,那么isinstance(obj, type)必須為True;

  • 如果第二個(gè)參數(shù)是類型,那么issubclass(type2, type)必須為True,即第二個(gè)參數(shù)類型是第一個(gè)參數(shù)類型的子類。

super函數(shù)的第二個(gè)參數(shù)存在時(shí),其實(shí)現(xiàn)大概如以下:

def super(cls, inst):
    mro = inst.__class__.mro() # Always the most derived class
    return mro[mro.index(cls) + 1]

很明顯,super返回在第二個(gè)參數(shù)對應(yīng)類的MRO列表中,第一個(gè)參數(shù)type的下一個(gè)類的代理。因此,要求第一個(gè)參數(shù)type存在于第二個(gè)參數(shù)類的MRO是必要的,只有第一個(gè)參數(shù)類是第二個(gè)參數(shù)所對應(yīng)類的父類,才能保證。

super()
super函數(shù)是要求有參數(shù)的,不存在無參的super函數(shù)。在類定義中以super()方式調(diào)用,是一種省略寫法,由解釋器填充必要參數(shù)。填充的第一個(gè)參數(shù)是當(dāng)前類,第二個(gè)參數(shù)是self

super() => super(current_class, self)

所以,super()這種寫法注定只能在類定義中使用。

現(xiàn)在再來看上面的繼承關(guān)系:

class D(object):
    def test(self):
        print('test in D')class C(D):
    def test(self):
        print('test in C')        # super().test() # 與下面的寫法等價(jià)
        super(C, self).test() # 返回self對應(yīng)類的MRO中,類C的下一個(gè)類的代理class B(D):
    def test(self):
        print('test in B')        # super().test() # 與下面的寫法等價(jià)
        super(B, self).test() # 返回self對應(yīng)類的MRO中,類B的下一個(gè)類的代理class A(B, C):
    pass

因此:

b = B()
b.test() # 基于類B的MRO(B->D->object),類B中的super()代理Dprint('==========')
a = A()
a.test() # 基于類A的MRO(A->B->C->D->object),類B中的super()代理C

以上就是在繼承關(guān)系中引入新類,改變方法解析順序的實(shí)例。

super([type[, object-or-type]])的第二個(gè)參數(shù),對象和類還有一點(diǎn)區(qū)別:使用對象返回的是代理使用綁定方法,使用類返回的代理使用非綁定方法。
如:

b = B()super(B, b).test()super(B, B).test(b)

這兩種方式得到的結(jié)果是相同的,區(qū)別在于非綁定調(diào)用與綁定調(diào)用。

3. 最佳實(shí)踐

3.1 不可預(yù)測的調(diào)用

普通的函數(shù)或者方法調(diào)用中,調(diào)用者肯定事先知道被調(diào)用者所需的參數(shù),然后可以輕松的組織參數(shù)調(diào)用。但是在多繼承關(guān)系中,情況有些尷尬,使用super代理調(diào)用方法,編寫類的作者并不知道最終會(huì)調(diào)用哪個(gè)類的方法,這個(gè)類都可能尚未存在。

如現(xiàn)在一作者編寫了以下類:

class D(object):
    def test(self):
        print('test in D')        
class B(D):
    def test(self):
        print('test in B')        super().test()

在定義類D時(shí),作者完全不可能知道test方法中的super().test()最終會(huì)調(diào)用到哪個(gè)類。
因?yàn)槿绻髞碛腥嗽谶@個(gè)類體系的基礎(chǔ)上,引入了如下類:

class C(D):
    def test(self):
        print('test in C')        super().test()        
class A(B, C):
    passa = A()
a.test()

此時(shí)會(huì)發(fā)現(xiàn)類Btest方法中super().test()調(diào)用了非原作者編寫的類的方法。
這里test方法的參數(shù)都是確定的,但是在實(shí)際生產(chǎn)中,可能各個(gè)類的test方法都是不同的,如果新引入的類C需要不同的參數(shù):

class C(D):
    def test(self, param_c):
        print('test in C, param is', param_c)        super().test()        
class A(B, C):
    passa = A()
a.test()

B的調(diào)用方式調(diào)用類Ctest方法肯定會(huì)失敗,因?yàn)闆]有提供任何參數(shù)。類C的作者是不可能去修改類B的實(shí)現(xiàn)。那么,如何適應(yīng)這種參數(shù)變換的需求,是在設(shè)計(jì)Python類中需要考慮的問題。

3.2 實(shí)踐建議

事實(shí)上,這種參數(shù)的變換在構(gòu)造方法上能體現(xiàn)得淋漓盡致,如果子類沒有正確初始化父類,那么子類甚至不能從父類繼承到需要的實(shí)例屬性。

所以,Python的類必須設(shè)計(jì)友好,才能拓展,有以下三條指導(dǎo)原則:

  1. 通過super()調(diào)用的方法必須存在;

  2. 調(diào)用者和被調(diào)用者參數(shù)必須匹配;

  3. 所有對父類方法的調(diào)用都必須使用super()

3.3 參數(shù)匹配

super()代理的類是不可預(yù)測的,需要匹配調(diào)用者和可能未知的調(diào)用者的參數(shù)。

固定參數(shù)
一種方法是使用位置參數(shù)固定函數(shù)簽名。就像以上使用的test()一樣,其簽名是固定的,只要要傳遞固定的參數(shù),總是不會(huì)出錯(cuò)。

關(guān)鍵字參數(shù)
每個(gè)類的構(gòu)造方法可能需要不同的參數(shù),這時(shí)固定參數(shù)滿足不了這種需求了。幸好,Python中的關(guān)鍵字參數(shù)可以滿足不定參數(shù)的需求。設(shè)計(jì)函數(shù)參數(shù)時(shí),參數(shù)由關(guān)鍵字參數(shù)和關(guān)鍵字參數(shù)字典組成,在調(diào)用鏈中,每一個(gè)函數(shù)獲取其所需的關(guān)鍵字參數(shù),保留不需要的參數(shù)到**kwargs中,傳遞到調(diào)用鏈的下一個(gè)函數(shù),最終**kwargs為空時(shí),調(diào)用調(diào)用鏈中的最后一個(gè)函數(shù)。

示例:

class Shape(object):
    def __init__(self, shapename, **kwargs):
        self.shapename = shapename        super().__init__(**kwargs)class ColoredShape(Shape):
    def __init__(self, color, **kwargs):
        self.color = color        super().__init__(**kwargs)

cs = ColoredShape(color='red', shapename='circle')

參數(shù)的剝落步驟為:

  • 使用cs = ColoredShape(color='red', shapename='circle')初始化ColoredShape;

  • ColoredShape__init__方法獲取其需要的關(guān)鍵字參數(shù)color,此時(shí)的kwargs{shapename:'circle'};

  • 調(diào)用調(diào)用鏈中Shape__init__方法,該方法獲取所需關(guān)鍵字參數(shù)shapename,此時(shí)kwargs{};

  • 最后調(diào)用調(diào)用鏈末端objet.__init__,此時(shí)因?yàn)?code style="line-height: 1.8; margin: 1px 5px; vertical-align: middle; display: inline-block; font-family: "Courier New", sans-serif !important; font-size: 12px !important; background-color: rgb(245, 245, 245) !important; border: 1px solid rgb(204, 204, 204) !important; padding: 0px 5px !important; border-radius: 3px !important;">kwargs已經(jīng)為空。

初始化子類傳遞的關(guān)鍵字參數(shù)尤為重要,如果少傳或多傳,都會(huì)導(dǎo)致初始化不成功。只有MRO中每個(gè)類的方法都是用super()來調(diào)用“父類”方法時(shí),才能保證super()調(diào)用鏈不會(huì)斷掉。

3.4 保證方法存在

上面的例子中,由于頂層父類object總是存在__init__方法,在任何MRO鏈中也總是最后一個(gè),因此任意的super().__init__調(diào)用總能保證是object.__init__結(jié)束。

但是其他自定義的方法得不到這樣的保證。這時(shí)需要手動(dòng)創(chuàng)建類似object的頂層父類:

class Root:
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(), 'draw')class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename        super().__init__(**kwds)    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)        super().draw()class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color        super().__init__(**kwds)    def draw(self):
        print('Drawing.  Setting color to:', self.color)        super().draw()

cs = ColoredShape(color='blue', shapename='square')
cs.draw()

如果有新的類要加入到這個(gè)MRO體系,新的子類也要繼承Root,這樣,所有的對draw()的調(diào)用都會(huì)經(jīng)過Root,而不會(huì)到達(dá)沒有draw方法的object了。這種對于子類的擴(kuò)展要求,應(yīng)當(dāng)詳細(xì)注明在文檔中,便于使用者閱讀。這種限制與Python所有異常都必須繼承自BaseException一樣。

3.5 組合不友好的類

對于那些不友好的類:

class Moveable:
    def __init__(self, x, y):
        self.x = x        self.y = y    def draw(self):
        print('Drawing at position:', self.x, self.y)

如果希望使用它的功能,直接將其加入到我們友好的繼承體系中,會(huì)破壞原有類的友好性。
除了通過繼承獲得第三方功能外,還有一種稱之為組合的方式,即把第三方類作為組件的方式揉入類中,使得類具有第三方的功能:

class MoveableAdapter(Root):
    def __init__(self, x, y, **kwds):
        self.movable = Moveable(x, y)        super().__init__(**kwds)    def draw(self):
        self.movable.draw()        super().draw()

Moveable被作為組件整合到適配類MoveableAdapter中,適配類擁有了Moveable的功能,而且是友好實(shí)現(xiàn)的。完全可以通過繼承適配類的方式,將Moveable的功能加入到友好的繼承體系中:

class MovableColoredShape(ColoredShape, MoveableAdapter):
    passMovableColoredShape(color='red', shapename='triangle',
                    x=10, y=20).draw()

http://www.cnblogs.com/crazyrunning/p/7095014.html