[譯]Python受推崇的super!

若是你沒有被Python內置的 super() 驚豔到,那頗有多是你並無真正瞭解它可以作什麼,以及如何高效地使用它。
關於 super() 的文章已經有不少了,其中不少文章以失敗了結。這篇文章嘗試經過如下幾種方式來改變這種情形:html

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

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

咱們使用 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() 會更好一些,由於它是經過計算獲得的非直接引用。app

非直接引用的好處之一是咱們沒必要經過具體的類名來指定執行操做的對象。若是你修改源代碼,將原來的基類變成別的類,那麼 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)複製代碼

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

這個計算由調用 super 的類和它的祖先樹共同決定。第一個要素,也就是調用 super 的類,是由實現這個類的源代碼所決定。在咱們的示例中, super() 是在 LoggingDict.__setitem__ 方法中被調用。這個要素是固定的。第二個要素,也是更有趣的要素,就是變量(咱們能夠建立新的子類,讓這個子類具備豐富的祖先樹))。wordpress

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

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

咱們構建的新類的祖先樹是: LoggingOD, LoggingDict, OrderedDict, dict, object。對於咱們的目標來講,重要的結果是 OrderedDict 被插入到 LoggingDict 以後,而且在 dict 以前。這意味着如今 LoggingDict.__setitem__ 中的 super() 調用把更新鍵值對的工做交給了 OrderedDict 而不是 dictoop

稍微思考一下這個結果。咱們以前並無替換掉 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…

相關文章
相關標籤/搜索