python繼承細節

不要子類化內置類型

內置類型(由C語言編寫)不會調用用戶定義的類覆蓋的特殊方法。python

例如,子類化dict做爲測驗:編程

class DoppeDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value]*2)   #改成重複存入的值

dd = DoppeDict(one=1)
print(dd)
dd['two'] = 2
print(dd)
dd.update(three=3)
print(dd)

#結果
{'one': 1}    #沒有預期效果,即__init__方法忽略了覆蓋的__setitem__方法
{'one': 1, 'two': [2, 2]}     #[]正確調用
{'one': 1, 'three': 3, 'two': [2, 2]}   #update方法忽略了覆蓋的__setitem__方法

原生類型這種行爲違背了面向對象編程的一個基本原則:始終應該從實例所屬的類開始搜索方法,即便在超類實現類的調用也是如此。這種環境中,有個特例,即__miss__方法能按預期工做。安全

不止實例內部的調用有這個問題,,內置類型的方法調用其餘類的方法,若是被覆蓋了,也不會被調用。例如:app

class AnswerDict(dict):
    def __getitem__(self, item):   #無論傳入什麼鍵,始終返回42
        return 42


ad = AnswerDict(a='foo')
print(ad['a'])
d = {}
d.update(ad)
print(d['a'])
print(d)

#結果
42       #符號預期
foo      #update忽略了覆蓋的__getitem__方法  
{'a': 'foo'}

於是子類化內置類型(dict,list,str)等容易出錯,內置類型的方法一般會忽略用戶覆蓋的方法。框架

不要子類化內置類型,用戶自定義的類應該繼承collections模塊中的類,例如Userdict,UserList,UserString,這些類作了特殊設計,所以易於擴展:函數

import collections

class AnswerDict(collections.UserDict):
    def __getitem__(self, item):   #無論傳入什麼鍵,始終返回42
        return 42


ad = AnswerDict(a='foo')
print(ad['a'])
d = {}
d.update(ad)
print(d['a'])
print(d)

#結果沒有問題
42
42
{'a': 42}

多重繼承和方法解析順序

任何實現多繼承的語言都要處理潛在的命名衝突,這種衝突由不相關的祖先類實現同名方法引發。這種衝突稱爲菱形問題:spa

定義四個類ABCD:設計

class A:
    def ping(self):
        print('A_ping:', self)

class B(A):
    def pong(self):
        print('B_pong:', self)

class C(A):
    def pong(self):
        print('C_pong:', self)

class D(B, C):

    def ping(self):
        super().ping()
        print('D_ping:', self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)

實箭頭表示繼承順序,虛箭頭表示方法解析順序,如圖所示:code

在D實例上調用pong方法:對象

if __name__ == '__main__':
    d = D()
    print(d.pong())
    print(C.pong(d))

#結果
B_pong: <__main__.D object at 0x0000026F4D9F5898>
C_pong: <__main__.D object at 0x0000026F4D9F5898>

按照解析順序,,直接調用d.pong()運行的是B類中的版本;

超類中的方法均可以調用,只要把實例做爲顯式參數傳入,如上面的C.pong(d)

python能區分d.pong()調用的是哪一個方法,是由於python會按照特定的順序便利繼承圖。這個方法叫作方法解析順序(本例中的解析順序如虛箭頭所示)。類都有一個名爲__mro__的屬性,它的值是一個元組,按照方法解析順序列出各個超類,從當前類一直往上,直到object。D類的__mro__:

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

D->B->C->A->object

若想把方法調用委託給超類,推薦的方式是使用內置的super()函數。然而有時須要繞過方法解析順序,直接調用某個超類的方法,例如D.ping方法這樣寫:

    def ping(self):
        A.ping(self)
        print('D_ping:', self)

這樣就調用了A的ping()方法繞過了B。

但仍然推薦使用super(),它更安全也不易過期。super()方法調用時,會遵照方法解析順序:

if __name__ == '__main__':
    d = D()
    print(d.ping())

#結果,兩次調用
#1調用super().ping(),super()函數把ping調用委託給A類(B,C沒有ping方法)
#2調用print('D_ping',self)
A_ping: <__main__.D object at 0x000001E1447358D0>
D_ping: <__main__.D object at 0x000001E1447358D0>

pingpong方法的5個調用:

if __name__ == '__main__':
    d = D()
    print(d.pingpong())

#結果
#1調用self.ping
#2調用self.ping內部的super.ping
#3調用super().ping
#4調用self.pong(),根據__mro__,找到B的pong
#5調用super().pong(),根據__mro__,找到B的pong
#6調用C.pong(self),忽略__mro__,調用C類的pong
A_ping: <__main__.D object at 0x00000204691F6898>
D_ping: <__main__.D object at 0x00000204691F6898>
A_ping: <__main__.D object at 0x00000204691F6898>
B_pong: <__main__.D object at 0x00000204691F6898>
B_pong: <__main__.D object at 0x00000204691F6898>
C_pong: <__main__.D object at 0x00000204691F6898>

方法解析順序不只考慮繼承圖,還考慮子類聲明中列出的超類的順序。若是把D類聲明爲class D(C, B):, 那麼__mro__中就是:D->C->B->A->object

分析類時查看__mro__屬性能夠看到方法解析順序:

bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)

import numbers
numbers.Integral.__mro__
(<class 'numbers.Integral'>, <class 'numbers.Rational'>, <class 'numbers.Real'>, <class 'numbers.Complex'>, <class 'numbers.Number'>, <class 'object'>)

#Base結尾命名的是抽象基類
import io
io.TextIOWrapper.__mro__
(<class '_io.TextIOWrapper'>, <class '_io._TextIOBase'>, <class '_io._IOBase'>, <class 'object'>)

處理多重繼承

一些建議:

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

使用多重繼承時,必定要明確一開始爲何要建立子類。主要緣由可能有:

1)實現接口,建立子類型,實現"是什麼"關係

2)繼承實現,經過重用避免代碼重複

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

2.使用抽象基類顯式表示接口

若是類做用是定義接口,應該明確把它定義爲抽象基類,建立abc.ABC或其餘抽象基類的子類。

3.經過混入重用代碼

若是一個類做用是爲多個不相關的子類提供方法實現,從而實現重用,但不體現"是什麼"關係,應該明確把那個類定義爲混入類。混入類不能實例化,具體類不能只繼承混入類。混入類應該提供某方面特定行爲,只實現少許關係很是緊密的方法。

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

在名稱中加入Mixin後綴。

5.抽象基類能夠做爲混入,反過來則不成立

抽象基類能夠實現具體方法,所以也能夠做爲混入使用。不過,抽象基類會定義類型,而混入作不到。此外,抽象基類能夠做爲其餘類的惟一基類,而混入類不行。

抽象基類有個侷限而混入類沒有:抽象基類中實現的具體方法只能與抽象基類以及其超類中的方法協做。

6.不要子類化多個具體類

具體類能夠沒有或者最多隻有一個具體超類。也就是說,具體類的超類中除了這一個具體超類以外,其他都是抽象基類或者混入。例如,下列代碼中,若是Alpha是具體類,那麼Beta和Gamma必須是抽象基類或者混入:

class MyConcreteclass(Alpha, Beta, Gamma):
     #更多代碼...

7.爲用戶提供聚合類

類的結構主要繼承自混入,自身沒有添加結構或者行爲,那麼這樣的類稱爲聚合類。

8.優先使用對象組合而不是繼承

組合和委託可以代替混入,把行爲提供給不一樣的類,可是不能取代接口繼承去定義類型層次結構。

以上來自《流暢的python》

相關文章
相關標籤/搜索