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

2018-02-24 15:26 更新

問題

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

解決方案

為了調(diào)用父類(超類)的一個方法,可以使用 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ù)的一個常見用法是在 ``__init__() 方法中確保父類被正確的初始化了:

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

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

super() 的另外一個常見用法出現(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__() 的實現(xiàn)包含一個名字檢查。如果某個屬性名以下劃線(_)開頭,就通過 super() 調(diào)用原始的 __setattr__() ,否則的話就委派給內(nèi)部的代理對象 self._obj 去處理。這看上去有點意思,因為就算沒有顯式的指明某個類的父類, super() 仍然可以有效的工作。

討論

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

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

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

盡管對于大部分代碼而言這么做沒什么問題,但是在更復(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__')

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

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

可能兩次調(diào)用 Base.__init__() 沒什么壞處,但有時候卻不是。另一方面,假設(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__')

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

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

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

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

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

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

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

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

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

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

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

如果你試著直接使用這個類就會出錯:

>>> 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'
>>>

但是,如果你使用多繼承的話看看會發(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() 實際上調(diào)用的是跟類A毫無關(guān)系的類B中的 spam() 方法。這個用類C的MRO列表就可以完全解釋清楚了:

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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號