[译]Python 受推崇的 super!

如果你没有被Python内置的 super() 惊艳到,那很有可能是你并没有真正了解它能够做什么,以及如何高效地使用它。
关于 super() 的文章已经有很多了,其中很多文章以失败告终。这篇文章尝试通过以下几种方式来改变这种情形:

  • 提供实际使用的例子
  • 对于工作原理给出清晰的模型
  • 每次都展示出使它工作的要点
  • 对于使用 super() 来创建类给出具体建议
  • 给出有帮助的真实示例而不是抽象的 ABCD 钻石图表

这篇文章中的示例包含 Python 2 语法Python 3 语法两种版本。

我们使用 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() 会更好一些,因为它是通过计算得到的非直接引用。

非直接引用的好处之一是我们不必通过具体的类名来指定执行操作的对象。如果你修改源代码,将原来的基类变成别的类,那么 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)

除了与外界相独立的改变之外,非直接引用还有一个主要的好处,而那些使用静态语言的人对此可能比较不熟悉。既然非直接引用是运行时才进行计算,那我们就可以自由地改变计算过程,让它指向其它类。

这个计算由调用 super 的类和它的祖先树共同决定。第一个要素,也就是调用 super 的类,是由实现这个类的源代码所决定。在我们的示例中, super() 是在 LoggingDict.__setitem__ 方法中被调用。这个要素是固定的。第二个要素,也是更有趣的要素,就是变量(我们可以创建新的子类,让这个子类具有丰富的祖先树))。

我们使用这个对我们有利的方法,来构建一个logging ordered dictionary,而不用修改已经存在的代码。

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

我们构建的新类的祖先树是: LoggingOD, LoggingDict, OrderedDict, dict, object。对于我们的目标来说,重要的结果是 OrderedDict 被插入到 LoggingDict 之后,并且在 dict 之前。这意味着现在 LoggingDict.__setitem__ 中的 super() 调用把更新键值对的工作交给了 OrderedDict 而不是 dict

稍微思考一下这个结果。我们之前并没有替换掉 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!

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注