Python super()深度思考

来源:互联网 发布:淘宝店铺信誉 编辑:程序博客网 时间:2024/06/01 07:55

如果你不曾被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。
>>> pprint(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):    passMovableColoredShape(color='red', shapename='triangle',                    x=10, y=20).draw()
一个复杂的例子--只是为了好玩

在Python 2.7和3.2中,collections模块都有一个Counter类和一个OrderedDict类。这些类可以轻易地组成一个OrderedCounter:
from collections import Counter, OrderedDictclass 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()的权威文章。

此文对本文翻译亦有帮助。

此文写的也特别好。

0 0
原创粉丝点击