繼承的優缺點


今天討論兩個話題

* 子類化內置類型的缺點

* 多重繼承和方法解析順序(__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、優先使用對象組合而不是類繼承

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM