Python super()深度思考

如果你不曾被Python的內置super()函數所折服,很可能是你不知道它能夠做什麼,或者如何有效地使用它。

很多文章寫過super(),但是很多寫的是錯的。此文試着在以下方面進行提高:

  • 提供實際用例
  • 給一個它如何工作的清楚的模型
  • 顯示每次讓它工作的關鍵技術(showing the tradecraft for getting it to work every time)
  • 給出在使用super()建立類時的具體建議
  • 通過實例而不是抽象的ABCD菱形圖表來說明
在本文中給出的例子可以在Python 2語法Python 3語法中找到。
 
使用Python 3語法,讓我們以一個基本的用例,即一個子類擴展了內置類的一個方法開始;
class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Setting to %r' % (key, value))
        super().__setitem__(key, value)
該類與它的父類dict具有相同的功能,但是它擴展了__setitem__方法,使得當一個值更新時有一個log入口。通過建立一個log入口,該方法使用super()函數去委派實際的根據鍵值對去更新字典的工作。
在介紹super()之前,其實我們可以通過dict.__setitem__(self, key, value)來調用。但是,使用super()會更好,因爲它是一個計算的間接引用。
間接引用的一個優勢是我們不用必須通過名指定委託類。如果你編輯源代碼來交換基類到其它的匹配,super()引用會自動跟隨。你有一個單一的真相來源:
class LoggingDict(SomeOtherMapping):            # new base class
    def __setitem__(self, key, value):
        logging.info('Setting to %r' % (key, value))
        super().__setitem__(key, value)         # no change needed
除了隔絕改變,computed indirection另一個主要的優勢是,有人可能不熟悉來自靜態語言的人。因爲間接性是在運行時計算的,我們有去影響計算的可能,使得間接性將指向其它的類。
計算既依賴於super被調用所在的類,也依賴於實例的祖先樹。第一個部分,super被調用的類,由該類的源代碼決定。在我們的例子中,super()在LoggingDict.__setitem__方法中被調用。該部分是固定的。第二個,也是更有趣的部分是可變的(我們可以在祖先樹中創建新的子類)。
讓我們使用到我們的優勢去構建一個logging有序的字典,不以修改現存類爲代價:
class LoggingOD(LoggingDict, collections.OrderedDict):
    pass
對我們的新類,祖先樹爲:LoggingOD, LoggingDict, OrderedDict, dict, object。針對我們的目的,重要的結果是OrderedDict位於LoggingDict之後,dict之前。這意味着super()調用在LoggingDict.__setitem__ 分派key/value更新到OrderedDict,而不是dict。
讓我們思考一下。我們並沒有改變LoggingDict的源代碼。而是我們建立了一個子類,該子類的唯一的邏輯就是比較已存在的兩個類並且控制它們的查找順序。
查找順序
 
我所叫的查找順序或者是祖先樹官方稱爲Method Resolution Order(MRO)。通過打印__mro__屬性可以很簡單地得到MRO。
>>> print(LoggingOD.__mro__)
(<class '__main__.LoggingOD'>,
 <class '__main__.LoggingDict'>,
 <class 'collections.OrderedDict'>,
 <class 'dict'>,
 <class 'object'>)
如果我們的目的是用我們喜歡的MRO創建一個子類,我們需要知道它是如何被計算的。原則很簡單。順序包括:它本身,它的基類,它的基類的基類,等等,直到到了object對象,這是所有類的基類。該序列的排序使得一個類總是出現在它的父類之前,如果有多個父類的話,它們保持與類定義時父類相同的順序。
上面顯示的MRO順序遵守這些限制:
  • LoggingOD先於它的父類,LoggingDict和OrderedDict
  • LoggingDict先於OrderedDict,因爲LoggingOD.__bases__是(LoggingDict, OrderedDict)
  • LoggingDict先於它的父類,即dict
  • OrderedDict先於它的父類,即dict
  • dict先於它的父類,即object
解決這些限制的過程被稱爲線性化(linearization)。關於這個主題有許多好的論文,但是創建一個我們想要的MRO的子類,我們只需要知道兩個限制即可:子類先於它的父類;需要遵守__bases__出現的順序。
實用的建議
 
super() 與委派方法調用一些類在實例的祖先樹中密切相關。要使重新排序方法調用能夠工作,類需要協作地設計。三個容易解決的實際問題:
  • 被super()調用的方法需要存在
  • 調用者和被調用者需要有一個匹配的參數標記
  • 方法的每一次出現均需要使用super()
1)我們首先看對得到調用者的參數匹配調用者方法的標記的策略。比起傳統的方法調用這有一些挑戰,在傳統的方法調用中,被調用者提前就知道了。使用super()函數,在類被寫入的時候被調用者是不知道的(因爲後面子類的寫入可能引入一個新類到MRO中)。
一種方法是堅持一個固定的標記使用位置參數。這在例如__setitem__的方法有兩個參數的固定標記,一個值和一個鍵。這個技巧應用在了LoggingDict的例子中,在LoggingDict和dict中__setitem__有相同的標記。
一個更有伸縮性的方法是讓在祖先樹中的每一個方法協作地設計去接受一個關鍵字參數和一個關鍵字參數字典,從而去除掉任何它需要的參數,剩餘的參數使用**kwds,最終使得在鏈條中的最終調用保留字典爲空。
每一個級別剝去它所需要的關鍵字參數,使得最終的空字典可以發送給一個方法,該方法根本不期待任何參數(例如,object.__init__需要0個參數):
class Shape:
    def __init__(self, shapename, **kwds):
        super.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()向前調用。
Root.draw也可以通過使用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()沒有路徑可以接近object,沒有被Root.draw所阻止。這應該清晰地寫入文檔,是的想要寫新的合作類的人知道子類應該來自於Root。這個限制與Python自己的需求,所有新的異常必須繼承自BaseException,沒有多大的不同,
3)上面的技巧確保了super()調用一個方法是知道確實存在的,而且標記也是正確的;但是,我們仍然依賴於super()在每一步被調用,使得委派鏈條持續不會斷裂。如果我們正在合作地設計類,這很容易取得,只是增加了一個super()調用在鏈條的每一個方法中。
以上所列的三個技巧提供了設計合作類的方法,使得可以根據子類組成或者重新排序。
如何集成一個非合作類
有時候,一個子類可能想使用合作的多繼承技術在一個第三方的類,而該類又不是爲它設計的(可能感興趣的方法沒有使用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層次,我們需要創建一個有一個必須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模塊都有一個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')

 

這篇文章翻譯自Raymond Hettinger寫的super()的權威文章

此文對本文翻譯亦有幫助。

此文寫的也特別好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章