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。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值