Python’s super() considered super!

参考:

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

在父类 dict 的基础上添加了 log 消息,然后通过调用父类的方法完成实际的工作。这种间接调用的一个好处是如果修改了基类,那么 super 访问的基类会随之改变。另一个好处是在动态语言中,因为行为是运行时决定的,所以可以在运行时修改类的行为。

对于类行为的影响因素有两个:super 所在的类和继承树。第一点,super 所在的类,是由类代码决定的。第二个方面,就比较有趣了。在不改变原有类的情况下来构造一个 logging ordered dict

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

新类的继承树是:LoggingOD, LoggingDict, OrderedDict, dict, object 重要的是 OrderedDict 在 LoggingDict 前,在 dict 后。这意味着 LoggingDict.__setitem__ 里面的 super 现在修改的是 OrderedDict 而不是 dict。 仅仅通过新增一个继承关系就能改变原有类的行为方式,想想就让人激动

Search Order

MRO 的生成遵循 C3 算法

Practical Advice

三条实用建议:

  • super 调用的方法必须存在
  • 调用和被调用函数签名必须匹配
  • 每个方法的调用都必须使用 super(而不是类名)
  1. 首先关注参数签名的匹配,第一种方法是所有全部使用一样的固定位置参数,显然不够灵活。一个更加灵活的方法是,层层嵌套的关键字参数,示例
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')
  1. 确保 super 调用的方法存在

对于 object 中有的方法,所有的 MRO 最终都会指向 object,所以最终会调用 object 中的相应方法,没有问题。对于 object 没有对应方法的函数,可以些一个 Root 类,将相应方法在 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,这种技术就和所有的 exceptions 都要从 BaseException 继承一样。

  1. 对于合作类,确保函数都通过 super 调用不是问题

How to Incorporate a Non-cooperative Class

有的时候,需要基于非合作的类来定制我们的代码,这时可以写一个 adapter class 比如有这样一个非合作类

class Moveable:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)

重写 adapter

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

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

  • 当继承一个 builtin 的时候,通常需要同时重载或扩展多个方法。比如在上面的例子中,__setitem__ 扩展就没有被其他如 dict.update 的方法使用,所以可能把这些方法也扩展一下比较好。这个问题并不是 super 独有的,而是继承 builtin 时普遍存在的。
  • 如果类存在嵌套继承,而我们需要确定的继承顺序,使用 assert 是个很好的选在,配合 index 方法
position = LoggingOD.__mro__.index
assert position(LoggingDict) < position(OrderedDict)
assert position(OrderedDict) < position(dict)

完整示例:

'Demonstrate effective use of super()'

import collections
import logging

logging.basicConfig(level='INFO')

class LoggingDict(dict):
    # Simple example of extending a builtin class
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        super().__setitem__(key, value)

class LoggingOD(LoggingDict, collections.OrderedDict):
    # Build new functionality by reordering the MRO
    pass

ld = LoggingDict([('red', 1), ('green', 2), ('blue', 3)])
print(ld)
ld['red'] = 10

ld = LoggingOD([('red', 1), ('green', 2), ('blue', 3)])
print(ld)
ld['red'] = 10
print('-' * 20)

# ------- Show the order that the methods are called ----------

def show_call_order(cls, methname):
    'Utility to show the call chain'
    classes = [cls for cls in cls.__mro__ if methname in cls.__dict__]
    print('  ==>  '.join('%s.%s' % (cls.__name__, methname) for cls in classes))

show_call_order(LoggingOD, '__setitem__')
show_call_order(LoggingOD, '__iter__')
print('-' * 20)

# ------- Validate and document any call order requirements -----

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

# ------- Getting the argument signatures to match --------------

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')

# -------- Making sure a root exists ----------------------------

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()

ColoredShape(color='blue', shapename='square').draw()
print('-' * 20)

# ------- Show how to incorporate a non-cooperative class --------

class Moveable:
    # non-cooperative class that doesn't use super()
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)

class MoveableAdapter(Root):
    # make a cooperative adapter class for Moveable
    def __init__(self, *, x, y, **kwds):
        self.moveable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.moveable.draw()
        super().draw()

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle', x=10, y=20).draw()

# -------- Complete example ------------------------------------

from collections import Counter, OrderedDict

class OrderedCounter(Counter, OrderedDict):
     'Counter that remembers the order elements are first encountered'

     def __repr__(self):
         return '%s(%r)' % (self.__class__.__name__, OrderedDict(self))

     def __reduce__(self):
         return self.__class__, (OrderedDict(self),)

oc = OrderedCounter('abracadabra')
print(oc)