python 多继承与super使用详解_继承中的MRO与super详解
Python進階-繼承中的MRO與super
寫在前面如非特別說明,下文均基于Python3
摘要
本文講述Python繼承關系中如何通過super()調用“父類”方法,super(Type, CurrentClass)返回CurrentClass的MRO中Type的下一個類的代理;以及如何設計Python類以便正確初始化。
1. 單繼承中父類方法調用
在繼承中,調用父類方法是很有必要的。調用父類方法的場景有很多:比如必須調用父類的構造方法__init__才能正確初始化父類實例屬性,使得子類實例對象能夠繼承到父類實例對象的實例屬性;
再如需要重寫父類方法時,有時候沒有必要完全摒棄父類實現,只是在父類實現前后加一些實現,最終還是要調用父類方法
單繼承是最簡單的繼承關系,多繼承過于復雜,而且使用起來容易出錯。因此一些高級語言完全摒棄了多繼承,只支持單繼承;一些高級語言雖然支持多繼承,但也不推薦使用多繼承。Python也是一樣,在不能完全掌握多繼承時,最好不好使用,單繼承能滿足絕大部分的需求。
1.1 非綁定方式調用
綁定方法與非綁定方法的區別與聯系參見:Python基礎-類
如有以下繼承關系兩個類:
class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
D.test(self)
現在要求在子類C的test函數中調用父類D的test實現。我們能想到最直接的方法恐怕是直接引用類對象D的函數成員test了:
class D(object):def test(self):print('test in D')class C(D):def test(self):print('test in C')
嘗試測試一下:
c = C()
c.test()
output:
test in C
test in D
看來非綁定的方式確實滿足了當前調用父類方法的需求。
1.2 builtin 函數 super
參考Python tutorial關于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函數返回委托類type的父類或者兄弟類方法調用的代理對象。super用來調用已經在子類中重寫了的父類方法。方法的搜索順序與getattr()函數相同,只是參數類type本身被忽略。
1.3 綁定方式調用
使用綁定方式調用父類方法,自然不能顯式傳入參數當前對象(self)。現在super函數能夠范圍對父類的代理,因為在單繼承中子類有且僅有一個父類,所以父類是明確的,我們完全清楚調用的父類方法是哪個:
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
事實上,super函數返回的代理對象是一個bultin class super,正如它的名字所指,類super代理了子類的父類。在單繼承關系中,super代理的類很容易找到嗎,就是子類的唯一父類;但是在多繼承關系中,super除了能代理子類的父類外,還有可能代理子類的兄弟類。
2.1 復雜的多繼承
在多繼承關系中,繼承關系可能會相當復雜。
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繼承層次結構如下:object
|
D
/ \
B C
\ /
A
類A的繼承關系中存在菱形結構,即可以通過多條路徑從類A到達某個父類,這里是D。
如果現在要求在類A中調用“父類”的test方法,需要一種對test方法的搜索解析順序,來決定到底是調用B,C或D的test方法。
2.2 方法解析順序(MRO)
上面提出的對test的方法的搜索順序,就是方法解析順序了。
深度優先
Python舊式類中,方法解析順序是深度優先,多個父類從左到右。
廣度優先
Python新式類中,方法解析順序是廣度優先,多個父類從左到右。
所以上面的解析順序是:A -> B -> C -> D -> object。
Python中,類的__mro__屬性展示了方法搜索順序,可以調用mro()方法或者直接引用__mro__得到搜索順序:
print(A.mro())print(A.__mro__)
output:
[, , , , ]
(, , , , )
所以
a = A()
a.test() # output: test in B
變化的MRO
即使是同一個類,在不同的MRO中位置的前后關系都是不同的。如以下類:
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的繼承層次結構為:object
|
D
/ \
C B
類B的MRO:B -> D -> object
對比類A的MRO:A -> B -> C -> D -> object
同樣的類B,在兩個不同的MRO中位置關系也是不同的。可以說,在已有的繼承關系中加入新的子類,會在MRO中引入新的類,并且改變解析順序。
那么可以想象,同樣在類B的test中通過super調用父類方法,在不同的MRO中實際調用的方法是不同的。
如下:
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
因為在原有的類關系中加入B和C的子類A,使得在B的test方法中調用super的test方法發生了改變,原來調用的是其父類D的test方法,現在調用的是其兄弟類C的test方法。
從這里可以看出super不總是代理子類的父類,還有可能代理其兄弟類。
因此在設計多繼承關系的類體系時,要特別注意這一點。
2.3 再看super方法
方法super([type[, object-or-type]]),返回的是對type的父類或兄弟類的代理。
如果第二個參數省略,返回的super對象是未綁定到確定的MRO上的:如果第二個參數是對象,那么isinstance(obj, type)必須為True;
如果第二個參數是類型,那么issubclass(type2, type)必須為True,即第二個參數類型是第一個參數類型的子類。
在super函數的第二個參數存在時,其實現大概如以下:
def super(cls, inst):
mro = inst.__class__.mro() # Always the most derived classreturn mro[mro.index(cls) + 1]
很明顯,super返回在第二個參數對應類的MRO列表中,第一個參數type的下一個類的代理。因此,要求第一個參數type存在于第二個參數類的MRO是必要的,只有第一個參數類是第二個參數所對應類的父類,才能保證。
super()
super函數是要求有參數的,不存在無參的super函數。在類定義中以super()方式調用,是一種省略寫法,由解釋器填充必要參數。填充的第一個參數是當前類,第二個參數是self:
super() => super(current_class, self)
所以,super()這種寫法注定只能在類定義中使用。
現在再來看上面的繼承關系:
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() # 返回self對應類的MRO中,類C的下一個類的代理class B(D):def test(self):print('test in B')# super().test() # 與下面的寫法等價super(B, self).test() # 返回self對應類的MRO中,類B的下一個類的代理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
以上就是在繼承關系中引入新類,改變方法解析順序的實例。
super([type[, object-or-type]])的第二個參數,對象和類還有一點區別:使用對象返回的是代理使用綁定方法,使用類返回的代理使用非綁定方法。
如:
b = B()super(B, b).test()super(B, B).test(b)
這兩種方式得到的結果是相同的,區別在于非綁定調用與綁定調用。
3. 最佳實踐
3.1 不可預測的調用
普通的函數或者方法調用中,調用者肯定事先知道被調用者所需的參數,然后可以輕松的組織參數調用。但是在多繼承關系中,情況有些尷尬,使用super代理調用方法,編寫類的作者并不知道最終會調用哪個類的方法,這個類都可能尚未存在。
如現在一作者編寫了以下類:
class D(object):def test(self):print('test in D')
class B(D):def test(self):print('test in B')super().test()
在定義類D時,作者完全不可能知道test方法中的super().test()最終會調用到哪個類。
因為如果后來有人在這個類體系的基礎上,引入了如下類:
class C(D):def test(self):print('test in C')super().test()
class A(B, C):passa = A()
a.test()
此時會發現類B的test方法中super().test()調用了非原作者編寫的類的方法。
這里test方法的參數都是確定的,但是在實際生產中,可能各個類的test方法都是不同的,如果新引入的類C需要不同的參數:
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的調用方式調用類C的test方法肯定會失敗,因為沒有提供任何參數。類C的作者是不可能去修改類B的實現。那么,如何適應這種參數變換的需求,是在設計Python類中需要考慮的問題。
3.2 實踐建議
事實上,這種參數的變換在構造方法上能體現得淋漓盡致,如果子類沒有正確初始化父類,那么子類甚至不能從父類繼承到需要的實例屬性。
所以,Python的類必須設計友好,才能拓展,有以下三條指導原則:通過super()調用的方法必須存在;
調用者和被調用者參數必須匹配;
所有對父類方法的調用都必須使用super()
3.3 參數匹配
super()代理的類是不可預測的,需要匹配調用者和可能未知的調用者的參數。
固定參數
一種方法是使用位置參數固定函數簽名。就像以上使用的test()一樣,其簽名是固定的,只要要傳遞固定的參數,總是不會出錯。
關鍵字參數
每個類的構造方法可能需要不同的參數,這時固定參數滿足不了這種需求了。幸好,Python中的關鍵字參數可以滿足不定參數的需求。設計函數參數時,參數由關鍵字參數和關鍵字參數字典組成,在調用鏈中,每一個函數獲取其所需的關鍵字參數,保留不需要的參數到**kwargs中,傳遞到調用鏈的下一個函數,最終**kwargs為空時,調用調用鏈中的最后一個函數。
示例:
class Shape(object):def __init__(self, shapename, **kwargs):self.shapename = shapenamesuper().__init__(**kwargs)class ColoredShape(Shape):def __init__(self, color, **kwargs):self.color = colorsuper().__init__(**kwargs)
cs = ColoredShape(color='red', shapename='circle')
參數的剝落步驟為:使用cs = ColoredShape(color='red', shapename='circle')初始化ColoredShape;
ColoredShape的__init__方法獲取其需要的關鍵字參數color,此時的kwargs為{shapename:'circle'};
調用調用鏈中Shape的__init__方法,該方法獲取所需關鍵字參數shapename,此時kwargs為{};
最后調用調用鏈末端objet.__init__,此時因為kwargs已經為空。
初始化子類傳遞的關鍵字參數尤為重要,如果少傳或多傳,都會導致初始化不成功。只有MRO中每個類的方法都是用super()來調用“父類”方法時,才能保證super()調用鏈不會斷掉。
3.4 保證方法存在
上面的例子中,由于頂層父類object總是存在__init__方法,在任何MRO鏈中也總是最后一個,因此任意的super().__init__調用總能保證是object.__init__結束。
但是其他自定義的方法得不到這樣的保證。這時需要手動創建類似object的頂層父類:
class Root:def draw(self):# the delegation chain stops hereassert not hasattr(super(), 'draw')class Shape(Root):def __init__(self, shapename, **kwds):self.shapename = shapenamesuper().__init__(**kwds)def draw(self):print('Drawing. Setting shape to:', self.shapename)super().draw()class ColoredShape(Shape):def __init__(self, color, **kwds):self.color = colorsuper().__init__(**kwds)def draw(self):print('Drawing. Setting color to:', self.color)super().draw()
cs = ColoredShape(color='blue', shapename='square')
cs.draw()
如果有新的類要加入到這個MRO體系,新的子類也要繼承Root,這樣,所有的對draw()的調用都會經過Root,而不會到達沒有draw方法的object了。這種對于子類的擴展要求,應當詳細注明在文檔中,便于使用者閱讀。這種限制與Python所有異常都必須繼承自BaseException一樣。
3.5 組合不友好的類
對于那些不友好的類:
class Moveable:def __init__(self, x, y):self.x = xself.y = ydef draw(self):print('Drawing at position:', self.x, self.y)
如果希望使用它的功能,直接將其加入到我們友好的繼承體系中,會破壞原有類的友好性。
除了通過繼承獲得第三方功能外,還有一種稱之為組合的方式,即把第三方類作為組件的方式揉入類中,使得類具有第三方的功能:
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的功能,而且是友好實現的。完全可以通過繼承適配類的方式,將Moveable的功能加入到友好的繼承體系中:
class MovableColoredShape(ColoredShape, MoveableAdapter):passMovableColoredShape(color='red', shapename='triangle',
x=10, y=20).draw()
參考
Python’s super() considered super!
Python tutorial#super
總結
以上是生活随笔為你收集整理的python 多继承与super使用详解_继承中的MRO与super详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 钱站显示放款中就一定会下款吗
- 下一篇: 新宝股份是什么概念 相关概念板块解析