Python’s super() considered super!

Python’s super() considered super!

原文作者:Raymond Hettinger
翻譯:@vision9527
時間:2016年12月
原文出處:https://rhettinger.wordpress.com/2011/05/26/super-considered-super/
說明:本文翻譯自國外寫的最好的一篇關於super的介紹,另外還有一篇寫的也很好的博文是Python’s Super Considered Harmful,自己可以搜尋看看。
如果你還沒有被python的super內置函數驚訝到,要麼你不知道它能做什麼要麼你不會高效的使用它。很多關於super的文章寫的都很不好,現在本文從以下幾點來提高:

  • 提供實際的例子
  • 給出super清晰的執行思路模型
  • 展示出使用super的技巧
  • 總結一些在實際創建類時使用super的建議
  • 通過簡單的ABCD菱形圖來解釋
    本文的解釋適用與python2.x和python3.x。使用python3的語法,通過以下基本的例子,從內建的類中建立一個子類來擴展功能的例子開始討論:
class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Settingto %r' % (key, value))
        super().__setitem__(key, value)

LoggingDict類不僅具有它的父類字典的所有功能,而且還爲setitem方法對象增加日誌功能,無論任何時候key被更新。LoggingDict的方法對象setitem在加入了日誌功能後,再通過super()把更新鍵值對的執行動作交給dict的setitem方法對象。在沒有super()以前,我們只能通過dict.setitem(self, key, value)這樣固定的方式來調用。然而super()會比這種方式更好,因爲它是使用一種計算間接取值引用(譯者注:動態的更新查找基類)。計算間接取值引用的好處之一就是我們不需要通過特定的類名來調用某個類。如果你編輯的源代碼從原來的基類切換到其它的對應類,super()引用會自動跟隨執行。例如下面這個例子:

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

除了這個單獨的變化外,計算間接取值的另一個主要的好處是那些使用靜態語言的人所不熟悉的。由於間接取值計算是在運行期間,因此我們可以自由的控制計算來達到使間接取值指向其它類的目的。
間接取值的計算取決於兩點:第一點是super()被調用的類對象,第二點是實例的繼承樹。第一點由源代碼中調用super的類所決定。在這個例子中,super()在LoggingDict.setitem方法對象被調用,這點是被固定住的部分。第二點就是我們感興趣的部分,它是可變的(因爲我們可以創建一個多繼承的子類)。
現在我們使用這個方法來構造一個有日誌功能和有序的字典,但是卻不需要修改已經存在的類。

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

新類的繼承樹是:LoggingOD, LoggingDict,OrderedDict, dict, object.對於我們的目的來說,最重要的結論就是OrderedDict是在LoggingDict 之後但卻是在dict之前插入。這就意味着super()現在的調用是在LoggingDict.setitem中,現在是使用OrderedDict而不是dict來更新鍵值對。
再思考一會兒,我們沒有改變LoggingDict的源代碼,反而是構建一個子類,它僅有的邏輯是將兩個存在的類組合並且控制它們的搜索順序。

Search Order

我一直聲稱的搜索順序或者繼承樹就是著名的Method Resolution Order(MRO)。通過打印mro屬性可以很方便的查看:

>>> pprint(LoggingOD.__mro__)
(<class '__main__.LoggingOD'>,
 <class '__main__.LoggingDict'>,
 <class 'collections.OrderedDict'>,
 <class 'dict'>,
 <class 'object'>)

如果我們的目標是創建一個遵循我們要求的MRO的子類,我們需要知道它是怎麼被計算的。這個基本原理是很簡單的。鏈包含此類,它的基類,基類的基類等等,直到object,因爲object是所有類的的根。 此鏈被有序排列,目的是子類總是出現在它的父類之前,並且如果有很多的父類(多繼承),基類的元祖保持相同的等級。
以上的MRO是遵循以下約束條件的順序鏈:

  • LoggingOD 在它的父類,即LoggingDict和OrdereDict之前
  • LoggingDict 在OrderedDict 之前,因爲LoggingOD的基類是(LoggingDict, OrderedDict)
  • LoggingDict 在它的父類dict之前
  • OrderedDict在它的父類dict之前
  • dict在它的父類object之前

解決這些限制條件的過程是線形技術。現在有很多關於這個主題的優秀論文,但是我們只需創建我們自己想要的MRO鏈就行,所以我們只需要知道兩個限制條件就可以:子類在基類之前,多繼承中基類按基類元祖的順序排列。

Practical Advice

super()做的事情是將方法對象的調用安排在實例的繼承樹中的某個類調用。爲了保證方法調用是按要求進行的,類必須被恰當的設計。以下呈現的三點就很容易的解決了實際問題:

  • 被super()調用的方法需要存在
  • 調用者和被調用者需要有匹配的參數
  • 每一個方法對象的出現都需要super()

(1)現在來看看讓調用者的參數匹配被調用方法的參數。這樣的難度比傳統方法需要提前聲明被調用的類要小。
使用super(),當有新類被創建時不知道真實被調用的類是誰(因爲以後創建子類可能會引入新類到MRO)

一個方法是使用位置參數固定的需要的參數。這個辦法在像setitem有兩個固定的參數(key,value)方法中是有用的。這個技術在LoggingDict的例子中有所體現,setitem有在LoggingDict和dict中的參數需求是相同的。
有一個靈活的方法是使在繼承樹中的每一個方法都被設計成接受可選參數和關鍵字參數,它們按需取參數,往前使用**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之前調用這些方法。根類的職責就是不讓super()繼續向前調用。
根類也可以用assertion來作爲保護辦法,確保它沒有屏蔽位於鏈後面位置的其它類的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()方法時停止在Root.draw()而不會有路徑搜索到object(因爲object沒有draw()方法)。因此需要清晰的說明文檔,讓想新創建類的人知道需要繼承自Root。 這個限制條件不同與python自己的要求,要求所有的異常必須從BaseException開始繼承。
(3)上面的技術確保super()調用的方法是已經存在,並且參數是正確的。然而我們仍然依賴super()在每一步的調用,目的是爲了確保調用鏈不被破壞。
如果我們設計類時就把super()的調用加入到鏈中類的每個方法中,這是很容易辦到的。
上面列出的三個技術是爲了設計協調的類,它們能被子類組合或者重排序。

How to Incorporate a Non-cooperative Class

有時候某個子類可能想使用的多重繼承類來自第三方,但是這些類都不是爲這個子類設計的(也許這些類中目的方法都不使用super()或者不繼承自相同根類)。這樣的情況可以通過遵循規則創建一個吸收類來解決。
例如,下面的Moveable類沒有使用super(),並且它有一個與object.init不相容的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類層級使用Moveable,我們需要創建一個吸收類來滿足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()

Complete Example – Just for Fun

在python2.7和python3.2中,collection模塊有Counter類 和一個OrderedDict類。這兩個類很容易會被組合成一個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')

Notes and References

*當父類是如dict()的內建類時,有時覆蓋或者擴展很多方法是非常必要的。在以上的例子當中,setitem不能被如dict.update方法所使用,所以擴展它是有必要的。這個要求對於super()不是唯一的,然而只要內建類被繼承這就會出現。
* 如果一個類依賴於某個父類而不是其它類(例如LoggingOD依賴於LoggingDict,LoggingDict出現在OrderedDict之前,OrderedDict出現在dict之前),這樣很容易加入斷言來驗證和標註文檔來說明想要的method resolution order:

position = LoggingOD.__mro__.index
assert position(LoggingDict) < position(OrderedDict)
assert position(OrderedDict) < position(dict)
  • 關於線形算法的好文可以在ython MRO documentation and at Wikipedia entry for C3 Linearization被找到。
  • Dylan編程語言有next-method construct對象,它類似於python中的super()。
  • 這篇博文的super()是用python3語法寫的。所有的源代碼可以在Recipe 577720被找到。還有,python2的super()僅僅在新式類(繼承自object或內建類)中有效。python2語法所有使用的源代碼在Recipe 577721。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章