今天討論兩個話題
* 子類化內置類型的缺點
* 多重繼承和方法解析順序(__mro__)
許多人都對繼承敬而遠之。Java不支持多繼承,並沒有產生什么壞的影響,而C++對多繼承的濫用上了很多人的心(筆者也是其中一位)。因此,今天就討論一下多繼承到底是怎么回事。
子類化內置類型很麻煩
直接子類化內置類型(如繼承list、dict、str)容易出錯,因為內置類型的方法通常會忽略用戶覆蓋的方法。因此不要子類化內置類型,用戶應該繼承collections模塊中的類,UserDict、UserList、UserString等,這些類是python提供給用戶用來擴展的。
多重繼承和方法解析順序
與繼承尤其是多繼承密切相關的另一個問題是:如果同級的父類有個同名方法或屬性,那么python如何決定使用哪一個?
作為一個曾經的C++程序員,經常要面臨這個問題。實際上,任何支持多繼承的語言都要面臨這種潛在的命名沖突,這種沖突由不相關的父類實現了同名的方法引起,這就是經典的”菱形問題“。
舉例說明如下:
1 class A:
2 def ping(self): 3 print("ping:", self) 4 5 class B(A): 6 def pong(self): 7 print("pong:", self) 8 9 class C(A): 10 def pong(self): 11 print("PONG:", self) 12 13 class D(B, C): 14 def ping(self): 15 super(D, self).ping() 16 print('post-ping:', self) 17 18 def pingpong(self): 19 self.ping() 20 print(1) 21 super(D, self).ping() 22 print(2) 23 self.pong() 24 print(3) 25 super(D, self).pong() 26 print(4) 27 C.pong(self)
類B、C繼承類A,且都實現了pong方法,但是打印的內容不一樣。
如果D的實例調用pong方法的話,調用的是C的還是B的呢?答案是B的pong方法。
類有一個名為__mro__的屬性,它是個元組,python會按照__mro__的值按照方法解析出各個父類,知道object類為止。如果想調用父類的方法,推薦使用super()函數。你也可以使用類名.方法(self)的方式調用父類的方法,但是不推薦,如果想繞過方法解析順序可以使用。
類的繼承關系和__mro__解析順序如下圖:
1 d = D()
2 d.ping() 3 print("-------------------------------") 4 d.pingpong() 5 6 7 """ 8 運行結果 9 ping: <__main__.D object at 0x00000000035F62E8> 10 post-ping: <__main__.D object at 0x00000000035F62E8> 11 ------------------------------- 12 ping: <__main__.D object at 0x00000000035F62E8> 13 post-ping: <__main__.D object at 0x00000000035F62E8> 14 1 15 ping: <__main__.D object at 0x00000000035F62E8> 16 2 17 pong: <__main__.D object at 0x00000000035F62E8> 18 3 19 pong: <__main__.D object at 0x00000000035F62E8> 20 4 21 PONG: <__main__.D object at 0x00000000035F62E8> 22 """
方法解析順序不僅跟繼承關系有關,還跟子類中聲明的父類順序有關。如果把類的聲明順序改變,那方法解析順序也會改變:
1 class E(B, C):
2 pass
3
4 class F(C, B): 5 pass 6 7 print(E.__mro__) 8 print(F.__mro__) 9 10 """ 11 (<class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) 12 (<class '__main__.F'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>) 13 """
方法解析順序依賴於C3算法。詳見https://www.python.org/download/releases/2.3/mro/
GUI工具包Tkinter的繼承關系圖如下:
加入Text的聲明順序是:
class Text(YView, XView, Widget):
...
那么方法解析順序就應該是:
Text -> YView -> XView -> Widget -> Grid -> Place -> Pack -> BaseWidget -> Misc -> object
使用多重繼承的一些建議
《設計模式:可復用面向對象軟件的基礎》中的適配器模式用的就是多重繼承(但是其他22個設計模式都是用單繼承,可見多重繼承顯然是不推薦使用)。
使用多重繼承容易得到不易理解和脆弱的系統設計,書中給出了一些關於繼承的建議:
1 把接口繼承跟實現繼承區分開
使用多重繼承時,一定要明確為什么創建子類,原因大概有二:
* 繼承接口,創建子類型,實現"是什么"的關系
* 繼承實現,避免代碼重復
避免代碼重復通常可以替換成組合和委托模式,二接口繼承則是框架的支柱。
2、使用抽象基類表示接口
如果類的作用是定義接口,應該把它明確聲明為抽象基類
3、通過混入重用代碼
如果類的作用是為不同的子類提供方法,從而實現重用,但子類不是"是什么"的關系,應該把這個類定義為混入類(mixin class)。混入類通常以xxxMixin命名,而且不能實例化,子類不能只繼承混入類。混入類應提供某方面的特定行為,只實現少了關系非常密切的方法。
4、在名稱中明確指明混入
5、抽象基類可以作為混入,反過來則不成立
抽象基類可以實現具體方法,所以可以作為混入類。然而,抽象基類可以派生子類,但是混入類不行。
6、不要子類化多個具體類
具體類做多這有一個具體超類。在具體類的繼承超類中,最多只有一個具體超類,其他則是抽象基類或者混入。假設有如下代碼:
1 class MyConcreteClass(Alpha, Beta, Gamma):
2 """
3 不要子類化多個具體類
4 """
如果Alpha是具體類, 那么Beta和Gamma都應該是抽象基類或混入。
7、為用戶提供聚合類
如果抽象基類或混入的組合對客戶代碼非常有用,那就提供一個類,用易於理解的方式把他們結合起來。
8、優先使用對象組合而不是類繼承