1. 程式人生 > >python繼承細節

python繼承細節

pre 及其 分享圖片 pytho 結構 查看 span rap ber

不要子類化內置類型

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

例如,子類化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__方法能按預期工作。

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

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}

多重繼承和方法解析順序

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

定義四個類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)

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

技術分享圖片

在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》

python繼承細節