python中super(译)

在阅读本文前,可以先行阅读”理解Python super “,以更快的理解python’s super

 如果你不喜欢Python 的内置类super,那么你有可能是不知道它能做什么或者你不能高效地使用它。

网上有很多写super()的文章,但是他们好像都写的不是很清楚,这篇文章将改变这个现状,我们将通过以下几个方面来讲super():

1、提供实际的使用案例

2、给出一个清楚的super工作模型

3、描述弄清它怎么工作的实验过程

4、给出使用super()创建类的具体建议

5、flavoring real examples over abstract ABCD diamond diagrams

这些实例来自Python2 syntax 和 Python  3 syntax

在这里,我们使用Python 3的语法,下面,我们以一个基础的实例开始我们的讲解,这是Python内建类dict的子类:

class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Settingto %r' % (key, value))
        super().__setitem__(key, value)

这个子类的功能跟他的父类一样,不同之处是在更新值时将键值写入日志。在写入日志之后,这个方法通过super()执行父类更新键值对的方法__setitem__。

在super()引入之前,我们可以用 dict.__setitem__(self, key, value)这种绑定死的调用去实现上述功能。super()更加灵活了,因为是一个被计算的间接引用(it is computed indirect reference。

间接寻址的一个好处是我们不用再使用类的名称来指定类,若你编辑源码程序改变了这个基类的名称,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(),除了隔离父类名字的变化外,另外一个好处是computed indirection,使用静态语言的人可能对这个不太熟悉。

因为indirection在程序运行时才计算,我们可以自由地影响计算从而使indirection指向其他的类。

计算这个indirection由调用super的类实例的祖先树决定。对于前者,是由python程序代码决定的,在上面的代码中,supper()被 LoggingDict类的__setitem__方法调用。LoggingDict类不变。但是我们可以新建一个新的子类,让这个子类继承更多的父类,从而改变这个indirection

下面,我们使用这个去构造一个日志有序的字典,并且不改变已经存在的类:

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

这个新类的祖先树为: LoggingOD, LoggingDict, OrderedDict, dict, object.

对于我们而言, 最重要的是让 OrderedDict插入在LoggingDict之后而在dict之前!这意味着LoggingDict.__setitem__调用supper()时,将传递key/value的更新到OrderedDict,而不是dict。

想象一下,我们没有去修改LoggingDict的源码,相反,我们通过创建一个子类,其唯一的逻辑是继承两个已经存在的类从而控制他们的搜索顺序(MRO),最后实现了给OrderedDict添加日志功能,真是令人激动呀!


搜索顺序(Search Order)

搜索顺序 或 祖先树  即通常所说的“方法解析顺序(Method Resolution or MRO)”,我们可以通过如下代码打印__mro__属性:

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

我们的目的是产生一个具有我们想要顺序的MRO的子类,所以我们需要知道MRO是怎么计算的。

MRO计算的原则其实很简单:MRO由类,基类, 基类的基类,直到根object类。

MRO排序满足以下要求:所有的类都出现在其父类之前,若有多个父类则保持原来的顺序。

上面的MRO的排序遵循如下约束:

1、LoggingOD先于父类LoggingDict与OrderedDict

2、LoggingDict先于OrderedDict类,因为在定义LoggingOD类时, LoggingDict在OrderedDict前面

3、LoggingDict先于其父类dict

4、OrderedDict先于其父类dict

5、dict先于其父类object

解决这些约束的过程称为线性化。有很多关于这个方面的优秀论文,但是要创建我们想要顺序的MRO子类,我们只需要知道两个约束:子类先于父类; 尊重__bases__的出现顺序



实践建议

super()的工作是把一些方法调用作用到实例祖先树中的一些类当中去。要让这些重新排序的方法调用工作,这些类需要一起设计。下面是能解决实际中问题的三个策略:

1) 调用者与被调用者参数必须一致

2) 被super()调用的方法必须存在

3) 每个相关方法必须同时调用super()

1) 我们先来看看这些策略。为了让调用者与被调用者的参数签名匹配,这里存在一些挑战。因为传统的方法调用中,被调用者是事先知道的。然后对于super(),被调用者直到创建类时才知道(因为子类的创建将引入新的类到MRO中)

解决问题的一种办法是使用固定的参数(a fixed signature using positional arguments),正如上面提到的方法__setitem__,其使用了两个固定参数 key 与 value。

另外一种更加灵活的办法是让所有在祖先树中的方法(method)都设计成关键字的参数或者是一个字典参数,移除其他的任何参数,使用**kwds来传递剩余参数,最后让链中最后调用的方法的字典为空(eventually leaving the dictionary empty for the final call in the chain)

每一层的调用剥离其需要的关键字参数,最后剩下一个空字典传递给最后那个无需参数的方法(object),代码示例如下:

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__调用结束。

换言之,supper()的目标调用被保证存在,不会出现AttributeError错误。

对于那些object没有的方法(比如 draw() 方法),我们需要写一个根类(root class)并保证其在object类之前被调用,这个root class作用就是阻止super()的目标方法传递给object,防止object不具有这个方法而发生异常。

Root.draw 可以使用防御编程的方式使用断言(an assertion)来确保不会防止目标方法向链中具有此方法的类中传递。如果一个子类错误地包含了一个没有继承Root类但具有draw()方法的类,这种情况就可能发生:

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类。这种限制于Python中新的异常必须继承BaseException相似。

3)以上策略保证了super()调用的方法必须存在,以及参数特征的一致性;然后我们仍然需要保证每一步都要调用supper(),从而防止MRO链在传递的过程中断掉。当然,如果我们一起设计所有的类,我们只需要在链中每个方法中加入super()即可。

以上三种技术提供了设计能被子类组合与重排的合作类(cooperative classes)的方法。



怎样整合非合作类(How to Incorporate a Non-cooperative Class)

有的时候,子类使用多继承技术继承一个原本没有为其设计的第三方类,这个类的相应方法可能没有使用super()或者不是从root class继承而来。这个时候,我们需要一个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类一起使用,我们需要设计一个带有super()调用额适配器(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()



一个完成的例子-Just for Fun

在Python 2.7 与 Python 3.2中,collections模块都有Counter class与 OrderedDict class,使用一个新类 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

* When subclassing a builtin such as dict(), it is often necessary to override or extend multiple methods at a time. In the above examples, the __setitem__ extension isn’t used by other methods such as dict.update, so it may be necessary to extend those also. This requirement isn’t unique to super(); rather, it arises whenever builtins are subclassed.

* If a class relies on one parent class preceding another (for example, LoggingOD depends on LoggingDict coming before OrderedDict which comes before dict), it is easy to add assertions to validate and document the intended method resolution order:

position = LoggingOD.__mro__.index assert position(LoggingDict) < position(OrderedDict)
assert position(OrderedDict) < position(dict)

* Good write-ups for linearization algorithms can be found at Python MRO documentation and at Wikipedia entry for C3 Linearization.

* The Dylan programming language has a next-method construct that works like Python’s super(). See Dylan’s class docs for a brief write-up of how it behaves.

* The Python 3 version of super() is used in this post. The full working source code can be found at:  Recipe 577720. The Python 2 syntax differs in that the type and object arguments to super() are explicit rather than implicit. Also, the Python 2 version of super() only works with new-style classes (those that explicitly inherit from object or other builtin type). The full working source code using Python 2 syntax is at Recipe 577721.

英文原文:Python’s super() Considerd Super

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
永久连接: http://www.nfvschool.cn/?p=638
标签:

发表评论