Python的super()被认为超级!
如果你不被Python的super()内置程序所震撼,那么你很可能不知道它能做什么或如何有效地使用它。
关于super()已经写了很多,而且大部分写作都是失败的。本文旨在通过以下方式改善这种情况:
1.提供实际用例
2.给出一个清晰的心理模型,了解它的工作原理
3.展示了每次使用它的工艺
4.构建使用super()的类的具体建议
5.赞成抽象ABCD钻石图的真实例子。
这篇文章的示例以Python 2语法和Python 3语法提供。
使用Python 3语法,让我们从一个基本用例开始,这是一个从一个内置类扩展方法的子类:
class LoggingDict(dict):
def __setitem__(self, key, value):
logging.info('Setting %r to %r' % (key, value))
super().__setitem__(key, value)
此类具有与其parent,dict相同的所有功能,但它会扩展__setitem__方法,以便在更新密钥时生成日志条目。在创建日志条目之后,该方法使用super()来委派实际使用键/值对更新字典的工作。
在引入super()之前,我们将使用dict .__ setitem __(self,key,value)硬连接调用。但是,super()更好,因为它是一个计算的间接引用。
间接的一个好处是我们不必按名称指定委托类。如果您编辑源代码以将基类切换到其他映射,则super()引用将自动跟随。你有一个单一的事实来源:
class LoggingDict(SomeOtherMapping): # new base class
def __setitem__(self, key, value):
logging.info('Setting %r to %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。出于我们的目的,重要的结果是在LoggingDict之后和dict之前插入了OrderedDict!这意味着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的子类,我们需要知道它是如何计算的。基础很简单。序列包括类,它的基类和这些基类的基类等等,直到到达所有类的根类对象。序列是有序的,所以一个类总是出现在它的父项之前,如果有多个父项,它们保持与基类元组相同的顺序。
上面显示的MRO是遵循这些约束的一个顺序:
LoggingOD在其父节点LoggingDict和OrderedDict之前
LoggingDict在OrderedDict之前,因为LoggingOD .__ bases__是(LoggingDict,OrderedDict)
LoggingDict在其父级之前是dict
OrderedDict在其父级之前是dict
dict在其父对象之前
解决这些约束的过程称为线性化。关于这个主题有很多好的论文,但是要根据我们的喜好创建MRO的子类,我们只需要知道两个约束:孩子在父母之前,并且__bases__中的出现顺序得到尊重。
使用建议
super()是将方法调用委托给实例的祖先树中的某个类的业务。对于可重新排序的方法调用,需要合作设计类。这提出了三个容易解决的实际问题:
super()调用的方法需要存在
调用者和被调用者需要具有匹配的参数签名
并且每次出现的方法都需要使用sup()
1)让我们首先看一下获取调用者参数以匹配被调用方法的签名的策略。这比预先知道被叫者的传统方法调用更具挑战性。使用super()时,在编写类时不知道被调用者(因为稍后编写的子类可能会将新类引入MRO)。
一种方法是使用位置参数来坚持使用固定签名。这适用于像__setitem__这样的方法,它们具有两个参数的固定签名,一个键和一个值。 LoggingDict示例中显示了此技术,其中__setitem__在LoggingDict和dict中具有相同的签名。
一种更灵活的方法是让祖先树中的每个方法协同设计为接受关键字参数和关键字参数字典,删除它需要的任何参数,并使用** kwds转发剩余的参数,最终将字典留空对于链中的最后一次调用。
每个级别剥离它需要的关键字参数,以便最终的空dict可以发送到一个根本不需要参数的方法(例如,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)看过让调用者/被调用者参数模式匹配的策略,现在让我们看看如何确保目标方法存在。
上面的例子显示了最简单的情况。我们知道对象有一个__init__方法,并且该对象始终是MRO链中的最后一个类,因此对super().__ init__的任何调用序列都保证以对.__ init__方法的调用结束。换句话说,我们保证super()调用的目标保证存在,并且不会因AttributeError而失败。
对于object没有感兴趣的方法(例如draw()方法)的情况,我们需要编写一个保证在object之前调用的根类。根类的责任就是在不使用super()进行转发调用的情况下使用方法调用。
Root.draw还可以使用断言来使用防御性编程,以确保它不会在链中稍后屏蔽某些其他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停止的情况下到达对象。这应该清楚地记录下来,以便编写新的合作类的人知道从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):
pass
MovableColoredShape(color='red', shapename='triangle',
x=10, y=20).draw()
完整示例 - 只是为了好玩
在Python 2.7和3.2中,collections模块同时具有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')
注释和参考
*当继承诸如dict()之类的内置函数时,通常需要一次覆盖或扩展多个方法。在上面的示例中,__setitem__扩展名不被其他方法(如dict.update)使用,因此可能还需要扩展它们。这个要求并不是super()所特有的;相反,只要内置子被子类化,它就会出现。
*如果一个类依赖于另一个前面的一个父类(例如,LoggingOD依赖于在dict之前出现的OrderedDict之前的LoggingDict),则很容易添加断言来验证和记录预期的方法解析顺序:
position = LoggingOD.__mro__.index
assert position(LoggingDict) < position(OrderedDict)
assert position(OrderedDict) < position(dict)
*线性化算法的优秀报告可以在Python MRO文档和维基百科C3线性化条目中找到。
*Dylan编程语言有一个下一个方法结构,就像Python的super()一样。请参阅Dylan的课程文档,简要介绍其行为方式。
*本文中使用了Python 3版本的super()。完整的工作源代码可以在以下位置找到:Recipe 577720.Python 2语法的不同之处在于super()的类型和对象参数是显式的而不是隐式的。此外,Python(2)版本的super()仅适用于新式类(显式继承自对象或其他内置类型的类)。使用Python 2语法的完整工作源代码位于Recipe 577721。