Python學習之路31-繼承的利弊

《流暢的Python》筆記

本篇是「面向對象慣用方法」的第五篇,咱們將繼續討論繼承,重點說明兩個方面:繼承內置類型時的問題以及多重繼承。概念比較多,較爲枯燥。python

1. 繼承內置類型

內置類型(C語言編寫)的方法一般會忽略用戶重寫的方法,這種行爲體如今兩方面:算法

  • 內置類型A的子類ChildA即便重寫了A中的方法,當ChildA調用這些方法時,也不必定調用的就是重寫的版本,而依然可能調用A中的版本;
  • 內置類型B調用ChildA的方法時,調用的也不必定是被ChildA重寫的方法,可能依然會調用A的版本。

dict__getitem__方法爲例,即便這個方法被子類重寫了,內置類型的get()方法也不必定調用重寫的版本:編程

# 代碼1.1
>>> class MyDict(dict):
...     def __getitem__(self, key):
...         return "Test"   # 無論要獲取誰,都返回"Test"
...    
>>> child = MyDict({"one":1, "two":2})
>>> child
{'one': 1, 'two': 2}    # 正常
>>> child["one"]
'Test'    # 此時也是正常的
>>> child.get("one")
1   # 這裏就不正常了,按理說應該返回"Test"
>>> b = {}
>>> b.update(child)
>>> b  # 並無調用child的__getitem__方法
{'one': 1, 'two': 2}

這是在CPython中的狀況,這些行爲其實違背了面向對象編程的一個基本原則,即應該始終從實例所屬的類開始搜索方法,即便在超類實現的類中調用也應該如此。但實際是可能直接調用基類的方法,而不先搜索子類。這種設定並不能說是錯誤的,這只是一種取捨,畢竟這也是CPython中的內置類型運行得快的緣由之一,但這種方式就給咱們出了難題。這種問題的解決方法有兩個:安全

  • 重寫從內置類型繼承來的全部方法(要真這樣,那我還繼承幹啥?),或者查看源碼,把相關的方法都給重寫了(誰的記性能這麼好?);
  • 第二種方法纔是推薦的方法:若是要繼承內置類型,請從collections模塊中繼承,好比繼承自UserListUserDictUserString。這些類不是用C語言寫的,而是用純Python寫的,而且嚴格遵循了上述面向對象的原則。若是上述代碼中的MyDict繼承自UserDict,行爲則會合乎預期。

強調:本節所述問題只發生在C語言實現的內置類型內部的方法委託上,並且隻影響直接繼承內置類型的自定義類。若是子類繼承自純Python編寫的類,則不會有此問題。微信

2.多重繼承

任何實現多重繼承的語言都要處理潛在的命名衝突,這種衝突由不相關的超類實現同名方法引發。這種衝突稱爲」菱形衝突「。框架

2.1 多重繼承的示例

下面是咱們要實現的類的UML圖:函數

紅線表示超類的調用順序,如下是它的實現:性能

# 代碼2.1
class A:
    def ping(self):
        print("ping in A:", self)

class B(A):
    def pong(self):
        print("pong in B:", self)

class C(A):
    def pong(self):
        print("PONG in C:", self)

class D(B, C):
    def ping(self):
        super().ping()
        print("ping in D:", self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)   # 在定義時調用特定父類的寫法,顯示傳入self參數

# 下面是它在控制檯中的調用狀況
>>> from diamond import *
>>> d = D()
>>> d.pong()
pong in B: <mytest.D object at 0x0000013E66313048>
>>> d.pingpong()
ping in A: <mytest.D object at 0x0000013E66313048>   # self.ping()
ping in D: <mytest.D object at 0x0000013E66313048>
ping in A: <mytest.D object at 0x0000013E66313048>   # super().ping()
pong in B: <mytest.D object at 0x0000013E66313048>   # self.pong()
pong in B: <mytest.D object at 0x0000013E66313048>   # super().pong()
PONG in C: <mytest.D object at 0x0000013E66313048>   # C.pong(self)
>>> C.pong(d)    # 在運行時調用特定父類的寫法,顯示傳入實例參數
PONG in C: <mytest.D object at 0x0000013E66313048>
>>> D.__mro__   # Method Resolutino Order,方法解析順序,上一篇文章中有所說起
(<class 'mytest.D'>, <class 'mytest.B'>, <class 'mytest.C'>, 
 <class 'mytest.A'>, <class 'object'>)

類都有一個名爲__mro__ 的屬性,它的值是一個元組,按必定順序列舉超類,這個順序由C3算法計算。網站

方法解析順序不只考慮繼承圖,還考慮子類聲明中列出超類的順序。例如,若是D類的聲明改成class D(C, B),那麼D則會先搜索C,再搜索Bspa

若想把方法調用委託給超類,推薦的作法是使用內置的super()函數;同時,還請注意上述調用特定超類的語法。然而,使用super()是最安全的,也不易過期。調用框架或不受本身控制的類層次結構中的方法時,尤爲應該使用super()

2.2 處理多重繼承的建議

繼承有不少用途,而多重繼承增長了可選方案和複雜度。使用多重繼承容易得出使人費解和脆弱的設計。如下是8條避免產生混亂類圖的建議:

  1. 把接口繼承和實現繼承區分開

    在使用多重繼承時,必定要明白本身爲何要建立子類:

    • 繼承接口,建立子類,實現「是什麼(」is-a」)」關係;
    • 繼承實現,經過重用避免代碼重複

其實這倆常常同時出現,不過只要有可能,必定要明確這麼作的意圖。經過繼承重用代碼是實現細節,一般能夠換成用組合和委託的模式,而接口繼承則是框架的支柱。

  1. 使用抽象基類顯示錶示接口

    若是類的做用是定義接口,應該將其明肯定義爲抽象基類。

  2. 經過「混入類」實現代碼重用

    若是一個類的做用是爲多個不相關的子類提供方法實現,從而實現重用,但不體現「is-a」關係,則應該把那個類明肯定義爲混入類(mixin class)。從概念上講,混入不定義新類型,只是打包方法,便於重用。混入類絕對不能實例化,並且具體類不能只繼承混入類。混入類應該提供某方面的特定行爲,只實現少許關係很是緊密的方法。

  3. 在名稱中明確指明混入

    因爲Python沒有把類明確聲明爲混入的正式方式,實際的作法是在類名後面加入Mixin後綴。Python的GUI庫Tkinter沒有采用這種方法,這也是它的類圖十分混亂的緣由之一,而Django則採用了這種方式。

  4. 抽象基類能夠做爲混入類,但混入類不能做爲抽象基類

    抽象基類能夠實現具體方法,所以能夠做爲混入類使用。但抽象基類能定義數據類型,混入類則作不到。此外,抽象基類能夠做爲其餘類的惟一基類,混入類則決不能做爲惟一的基類,除非這個混入類繼承了另外一個更具體的混入(這種作法很是少見)。

    但值得注意的是,抽象基類中的具體方法只是一種便利措施,由於它只能調用抽象基類及其超類中定義了的方法,那麼用戶自行調用這些方法也能夠實現一樣的功能,因此,抽象基類也並不常做爲混入類。

  5. 不要從多個具體類繼承

    應該儘可能保證具體類沒有或者最多隻有一個具體超類。也就是說,具體類的超類中除了這一個具體超類外,其他的都應該是抽象基類或混入類。

  6. 爲用戶提供聚合類

    若是抽象基類或混入類的組合對客戶代碼很是有用,那就提供一個類,使用易於理解的方式把它們結合起來,這種類被稱爲聚合類。好比tkinter.Widget類,它的定義以下:

    # 代碼2.2
    class Widget(BaseWidget, Pack, Place, Grid):  # 省略掉了文檔註釋
        pass

    它的定義體是空的,但經過這一個類,提供了四個超類的所有方法。

  7. 優先使用對象組合,而不是類繼承

    優先使用組合能讓設計更靈活。即使是單繼承,這個原則也能提高靈活性,由於繼承是一種緊耦合,並且較高的繼承樹容易倒。組合和委託還能夠代替混入類,把行爲提供給不一樣的類,但它不能取代接口繼承,由於接口繼承定義的是類層次結構。

迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~

相關文章
相關標籤/搜索