[譯]Python受推崇的super!

Wray_Zheng發表於2017-03-08

如果你沒有被Python內建的 super() 驚豔到,那很有可能是你並沒有真正瞭解它能夠做什麼,以及如何高效地使用它。
關於 super() 的文章已經有很多了,其中很多文章以失敗告終。這篇文章嘗試通過以下幾種方式來改變這種情形:

  • 提供實際使用的例子
  • 對於工作原理給出清晰的模型
  • 每次都展示出使它工作的要點
  • 對於使用 super() 來建立類給出具體建議
  • 給出有幫助的真實示例而不是抽象的 ABCD 鑽石圖表

這篇文章中的示例包含 Python 2 語法Python 3 語法兩種版本。

我們使用 Python 3 語法,從一個基礎的示例開始,一個擴充套件了Python內建型別方法的子類。

class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Settingto %r' % (key, value))
        super().__setitem__(key, value)複製程式碼

這個類擁有與它父類(字典)相同的所有功能,不過它擴充套件了 __setitem__ 方法,無論哪一個鍵被更新,該條目都會被記錄下來。記錄完更新的條目之後,該方法使用 super() 將更新鍵值對的實際工作委託給它的父類。

在介紹 super() 之前,我們可能會使用具體的類名來呼叫 dict.__setitem__(self, key, value) .但是, super() 會更好一些,因為它是通過計算得到的非直接引用。

非直接引用的好處之一是我們不必通過具體的類名來指定執行操作的物件。如果你修改原始碼,將原來的基類變成別的類,那麼 super() 引用會自動變成對應的基類。下面這個例項可以說明這一點:

# new base class
class LoggingDict(SomeOtherMapping):
    def __setitem__(self, key, value):
        logging.info('Settingto %r' % (key, value))
        # no change needed
        super().__setitem__(key, value)複製程式碼

除了與外界相獨立的改變之外,非直接引用還有一個主要的好處,而那些使用靜態語言的人對此可能比較不熟悉。既然非直接引用是執行時才進行計算,那我們就可以自由地改變計算過程,讓它指向其它類。

這個計算由呼叫 super 的類和它的祖先樹共同決定。第一個要素,也就是呼叫 super 的類,是由實現這個類的原始碼所決定。在我們的示例中, super() 是在 LoggingDict.__setitem__ 方法中被呼叫。這個要素是固定的。第二個要素,也是更有趣的要素,就是變數(我們可以建立新的子類,讓這個子類具有豐富的祖先樹))。

我們使用這個對我們有利的方法,來構建一個logging ordered dictionary,而不用修改已經存在的程式碼。

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass複製程式碼

我們構建的新類的祖先樹是: LoggingOD, LoggingDict, OrderedDict, dict, object。對於我們的目標來說,重要的結果是 OrderedDict 被插入到 LoggingDict 之後,並且在 dict 之前。這意味著現在 LoggingDict.__setitem__ 中的 super() 呼叫把更新鍵值對的工作交給了 OrderedDict 而不是 dict

稍微思考一下這個結果。我們之前並沒有替換掉 LoggingDict 的原始碼。相反,我們建立了一個子類,它的唯一邏輯就是將兩個已有的類結合起來,並控制它們的搜尋順序。

搜尋順序

我所說的搜尋順序或者祖先樹,正式的名稱是 方法解析順序,簡稱 MRO。通過列印 __mro__ 屬性,我們很容易就能獲取MRO。

>>> pprint(LoggingOD.__mro__)
(<class '__main__.LoggingOD'>,
 <class '__main__.LoggingDict'>,
 <class 'collections.OrderedDict'>,
 <class 'dict'>,
 <class 'object'>)複製程式碼

如果我們的目標是建立一個具有我們想要的MRO的子類,我們需要知道它是如何被計算出來的。基礎部分很簡單。這個序列包含了類本身,它的基類,以及基類的基類,一直到所有類的祖先類 object 。這個序列經過了排序,因此一個類總是出現在它的父類之前,如果有多個父類,它們保持與基類元組相同的順序。

上面展示的 MRO 遵循以下的限制:

  • LoggingOD 在它的父類 LoggingDictOrderedDict 之前
  • LoggingDictOrderedDict 之前,因為 LoggingOD.__base__ 的值為 (LoggingDict, OrderedDict)
  • LoggingDict 在它的父類 dict 之前
  • OrderedDict 在它的父類 dict 之前
  • dict 在它的父類 object 之前

解決這些限制的過程被稱為線性化, 關於這個話題有許多優秀的論文,但要建立具有我們想要的MRO的一個子類,我們只需要知道兩條限制:子類在父類之前、出現的順序遵從 __base__ 中的順序。

實用的建議

super() 的工作就是將方法呼叫委託給祖先樹中的某個類。要讓可重排列的方法呼叫正常工作,我們需要對這個類進行聯合的設計。這也顯露出了三個易於解決的實際問題:

  • super() 呼叫的方法必須存在
  • 呼叫者和被呼叫者需要具有相同的引數簽名
  • 該方法的每次呼叫都需要使用 super()

1)我們先來看看使呼叫者與被呼叫者的引數簽名相匹配的策略。比起傳統的方法呼叫(提前知道被呼叫者是誰),這會有一點點挑戰性。使用 super()編寫一個類時,我們並不知道被呼叫者是誰(因為之後編寫的子類可能會在 MRO 中引入新的類)。

一種方式是使用固定的簽名,也就是位置引數。像 __setitem__ 這樣的方法擁有兩個引數的固定簽名,一個鍵和一個值,這種情況下能夠很好地工作。這個技術在 LoggingDict 的示例中展示過,其中 __setitem__LoggingDictdict 中擁有同樣的引數簽名。

一種更加靈活的方式是將每一個祖先類中對應的方法都共同設計成接收關鍵字引數和一個關鍵字引數字典,將它需要的引數移除,並將剩餘的引數通過 **kwds 繼續傳遞,最終會在最後的呼叫中剩下一個空字典。

每一層都移除它所需要的關鍵字引數,最後的空字典可以被傳遞給一個不需要任何引數的方法(例如: object.__init__ 不需要任何引數)

class Shape:
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)        

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)

cs = ColoredShape(color='red', shapename='circle')複製程式碼

2) 看完了使呼叫者和被呼叫者的引數模式相匹配的策略,我們現在來看看如何確保目標方法存在。

上面的示例展示了最簡單的情況。我們知道 object 有一個 __init__ 方法,並且 object 永遠是 MRO 鏈中的最後一個類,所以任何呼叫 super().__init__ 的序列都會以呼叫 object.__init__ 方法作為結尾。換句話說,我們能確保 super() 呼叫的目標肯定存在,一定不會發生 AttributeError 的錯誤。

對於我們想要的方法在 object 中並不存在的情況(例如 draw() 方法),我們需要編寫一個一定會在 object 之前被呼叫的根類(root class)。這個根類的作用是在 object 之前將該方法吞噬掉,避免 super() 的繼續呼叫。

Root.draw 還能夠利用防禦式程式設計,通過使用 assertion 語句來確保它沒有遮蔽掉 MRO 鏈中的其它 draw() 呼叫。當一個子類錯誤地合併一個擁有 draw() 方法的類,但卻沒有繼承 Root 類時就可能發生這種情況:

class Root:
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(), 'draw')

class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)
        super().draw()

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting color to:', self.color)
        super().draw()

cs = ColoredShape(color='blue', shapename='square')
cs.draw()複製程式碼

如果子類想要將其它類插入到 MRO 鏈中,那麼那些被插入的類也需要繼承 Root ,以確保任何途徑下呼叫 draw() 方法都不會到達 object 類,而會被 Root.draw 所攔截而終止。

這一點應該清楚地寫到文件中,這樣一來如果有人編寫與之相關的類,就知道應該繼承 Root 類了。這一限制,與 Python 要求所有異常類都要繼承 BaseException 沒有多大區別。

3) 上面展示的技術假定了 super() 呼叫的是一個已知存在、並且引數簽名正確的方法。然而,我們仍依賴於 super() 在每一步中都被呼叫,代表鏈得以繼續不至於斷裂。我們如果聯合設計這些類,那麼這一點很容易達到——只需要在鏈中的每一個方法中都新增一個 super() 呼叫。

上面列出的三種技術,提供了一些方式讓我們設計出能夠通過子類來組合或重排序的聯合類。

如何合併一個非聯合(Non-cooperative)類

偶然情況下,一個子類可能想要對一個並非給它設計的第三方類使用聯合多繼承技術(可能該第三方類的有關方法並沒有使用 super() 或可能它並沒有繼承 Root 類)。這種情況可以通過建立一個符合規則的介面卡類(adapter class)來輕鬆解決。

例如,下面的 Moveable 類沒有呼叫 super() ,並且它的 __init__()object.__init__() 的簽名不相容,此外它還沒有繼承 Root

class Moveable:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)複製程式碼

如果我們想要將該類與我們聯合設計的 ColoredShape 分層結構(hierarchy)一起使用,我們需要建立一個介面卡,包含必要的 super() 呼叫:

class MoveableAdapter(Root):
    def __init__(self, x, y, **kwds):
        self.movable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.movable.draw()
        super().draw()

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle',
                    x=10, y=20).draw()複製程式碼

完整示例(只為樂趣)

在 Python 2.7 和 3.2 中,collections 模組有 CounterOrderedDict 兩個類。這兩個類可以容易地組合成一個 OrderedCounter 類:

from collections import Counter, OrderedDict

class OrderedCounter(Counter, OrderedDict):
     'Counter that remembers the order elements are first seen'
     def __repr__(self):
         return '%s(%r)' % (self.__class__.__name__,
                            OrderedDict(self))
     def __reduce__(self):
         return self.__class__, (OrderedDict(self),)

oc = OrderedCounter('abracadabra')複製程式碼

說明和引用

  • 當繼承內建的資料型別如 dict() 來建立子類的時候,通常有必要同時過載或擴充套件多個方法。在上面的示例中,__setitem__ 的擴充套件沒有被其它方法如 dict.update 所使用,因此也可能有必要對那些方法進行擴充套件。這一要求並非是 super() 所特有的,相反,任何通過繼承內建型別建立子類的情況都需要滿足這個要求。

  • 如果一個類依賴於一個父類,而這個父類又依賴於另一個類(例如,LoggingOD 依賴於 LoggingDict,而後者出現在 OrderedDict 之前,最後才是 dict),那麼很容易通過新增斷言(assertions)來驗證並記錄預計的方法解析順序(MRO):

      position = LoggingOD.__mro__.index
      assert position(LoggingDict) < position(OrderedDict)
      assert position(OrderedDict) < position(dict)複製程式碼
  • 關於線性化演算法的優秀文章可以參考 Python MRO documentationWikipedia entry for C3 Linearization

  • Dylan 程式語言有一個 next-method 建構函式,類似於 Python 的 super() 。有關它工作原理的簡短文章,請參考 Dylan's class docs

  • 這篇文章使用的是 Python 3 版本的 super()。全部的原始碼可以在此處獲取:Recipe 577720 。Python 2 語法的不同之處在於傳遞給 super() 方法的引數在型別和物件上是明確的。另外,Python 2 版本的 super() 只對新式的(new-style)類有效(即那些明確從某個物件或其它內建型別繼承的類)。使用 Python 2 語法的全部原始碼可以在此處獲取: Recipe 577721

致謝

數位 Python 開發者做了此文章發表前的審閱。他們的意見很大程度上提高了這篇文章的質量。

他們是:Laura Creighton, Alex Gaynor, Philip Jenvey, Brian Curtin, David Beazley, Chris Angelico, Jim Baker, Ethan Furman, and Michael Foord. Thanks one and all.

譯者補充

花了一些時間,終於翻譯完了這篇文章。原文中有一些地方本身寫得易於理解,但翻譯成中文會有點繞。由於水平有限,翻譯得不準確的地方還請大家指出,如果有什麼想法歡迎留言一起探討。

英文原文:Python's super() considered super!

版權資訊

譯者:Wray Zheng
譯文來源: www.codebelief.com/article/201…

相關文章