8.7 調(diào)用父類方法

2018-02-24 15:26 更新

問題

你想在子類中調(diào)用父類的某個(gè)已經(jīng)被覆蓋的方法。

解決方案

為了調(diào)用父類(超類)的一個(gè)方法,可以使用 super() 函數(shù),比如:

class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()  # Call parent spam()

super()``函數(shù)的一個(gè)常見用法是在 ``__init__() 方法中確保父類被正確的初始化了:

class A:
    def __init__(self):
        self.x = 0

class B(A):
    def __init__(self):
        super().__init__()
        self.y = 1

super() 的另外一個(gè)常見用法出現(xiàn)在覆蓋Python特殊方法的代碼中,比如:

class Proxy:
    def __init__(self, obj):
        self._obj = obj

    # Delegate attribute lookup to internal obj
    def __getattr__(self, name):
        return getattr(self._obj, name)

    # Delegate attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value) # Call original __setattr__
        else:
            setattr(self._obj, name, value)

在上面代碼中,__setattr__() 的實(shí)現(xiàn)包含一個(gè)名字檢查。如果某個(gè)屬性名以下劃線(_)開頭,就通過 super() 調(diào)用原始的 __setattr__() ,否則的話就委派給內(nèi)部的代理對(duì)象 self._obj 去處理。這看上去有點(diǎn)意思,因?yàn)榫退銢]有顯式的指明某個(gè)類的父類, super() 仍然可以有效的工作。

討論

實(shí)際上,大家對(duì)于在Python中如何正確使用 super() 函數(shù)普遍都知之甚少。你有時(shí)候會(huì)看到像下面這樣直接調(diào)用父類的一個(gè)方法:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

盡管對(duì)于大部分代碼而言這么做沒什么問題,但是在更復(fù)雜的涉及到多繼承的代碼中就有可能導(dǎo)致很奇怪的問題發(fā)生。比如,考慮如下的情況:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

class B(Base):
    def __init__(self):
        Base.__init__(self)
        print('B.__init__')

class C(A,B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)
        print('C.__init__')

如果你運(yùn)行這段代碼就會(huì)發(fā)現(xiàn) Base.__init__() 被調(diào)用兩次,如下所示:

>>> c = C()
Base.__init__
A.__init__
Base.__init__
B.__init__
C.__init__
>>>

可能兩次調(diào)用 Base.__init__() 沒什么壞處,但有時(shí)候卻不是。另一方面,假設(shè)你在代碼中換成使用 super() ,結(jié)果就很完美了:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        super().__init__()
        print('A.__init__')

class B(Base):
    def __init__(self):
        super().__init__()
        print('B.__init__')

class C(A,B):
    def __init__(self):
        super().__init__()  # Only one call to super() here
        print('C.__init__')

運(yùn)行這個(gè)新版本后,你會(huì)發(fā)現(xiàn)每個(gè) __init__() 方法只會(huì)被調(diào)用一次了:

>>> c = C()
Base.__init__
B.__init__
A.__init__
C.__init__
>>>

為了弄清它的原理,我們需要花點(diǎn)時(shí)間解釋下Python是如何實(shí)現(xiàn)繼承的。對(duì)于你定義的每一個(gè)類而已,Python會(huì)計(jì)算出一個(gè)所謂的方法解析順序(MRO)列表。這個(gè)MRO列表就是一個(gè)簡單的所有基類的線性順序表。例如:

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

為了實(shí)現(xiàn)繼承,Python會(huì)在MRO列表上從左到右開始查找基類,直到找到第一個(gè)匹配這個(gè)屬性的類為止。

而這個(gè)MRO列表的構(gòu)造是通過一個(gè)C3線性化算法來實(shí)現(xiàn)的。我們不去深究這個(gè)算法的數(shù)學(xué)原理,它實(shí)際上就是合并所有父類的MRO列表并遵循如下三條準(zhǔn)則:

  • 子類會(huì)先于父類被檢查
  • 多個(gè)父類會(huì)根據(jù)它們在列表中的順序被檢查
  • 如果對(duì)下一個(gè)類存在兩個(gè)合法的選擇,選擇第一個(gè)父類

老實(shí)說,你所要知道的就是MRO列表中的類順序會(huì)讓你定義的任意類層級(jí)關(guān)系變得有意義。

當(dāng)你使用 super() 函數(shù)時(shí),Python會(huì)在MRO列表上繼續(xù)搜索下一個(gè)類。只要每個(gè)重定義的方法統(tǒng)一使用 super() 并只調(diào)用它一次,那么控制流最終會(huì)遍歷完整個(gè)MRO列表,每個(gè)方法也只會(huì)被調(diào)用一次。這也是為什么在第二個(gè)例子中你不會(huì)調(diào)用兩次 Base.__init__() 的原因。

super() 有個(gè)令人吃驚的地方是它并不一定去查找某個(gè)類在MRO中下一個(gè)直接父類,你甚至可以在一個(gè)沒有直接父類的類中使用它。例如,考慮如下這個(gè)類:

class A:
    def spam(self):
        print('A.spam')
        super().spam()

如果你試著直接使用這個(gè)類就會(huì)出錯(cuò):

>>> a = A()
>>> a.spam()
A.spam
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 4, in spam
AttributeError: 'super' object has no attribute 'spam'
>>>

但是,如果你使用多繼承的話看看會(huì)發(fā)生什么:

>>> class B:
...     def spam(self):
...         print('B.spam')
...
>>> class C(A,B):
...     pass
...
>>> c = C()
>>> c.spam()
A.spam
B.spam
>>>

你可以看到在類A中使用 super().spam() 實(shí)際上調(diào)用的是跟類A毫無關(guān)系的類B中的 spam() 方法。這個(gè)用類C的MRO列表就可以完全解釋清楚了:

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

在定義混入類的時(shí)候這樣使用 super() 是很普遍的??梢詤⒖?.13和8.18小節(jié)。

然而,由于 super() 可能會(huì)調(diào)用不是你想要的方法,你應(yīng)該遵循一些通用原則。首先,確保在繼承體系中所有相同名字的方法擁有可兼容的參數(shù)簽名(比如相同的參數(shù)個(gè)數(shù)和參數(shù)名稱)。這樣可以確保 super() 調(diào)用一個(gè)非直接父類方法時(shí)不會(huì)出錯(cuò)。其次,最好確保最頂層的類提供了這個(gè)方法的實(shí)現(xiàn),這樣的話在MRO上面的查找鏈肯定可以找到某個(gè)確定的方法。

在Python社區(qū)中對(duì)于 super() 的使用有時(shí)候會(huì)引來一些爭議。盡管如此,如果一切順利的話,你應(yīng)該在你最新代碼中使用它。Raymond Hettinger為此寫了一篇非常好的文章“Python’s super() Considered Super!” ,通過大量的例子向我們解釋了為什么 super() 是極好的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)