您的位置:首页 > 编程语言 > Python开发

深入思考python的super()

2016-01-12 18:58 288 查看

0. 背景

本文翻译自文章 《Python’ssuper() considered super!》,这篇文章是博主在王晓斌博客中,看到的博客Python面向对象_super()函数。博客中的一个问题:
class Child(Parent):
def __init__(self):
Parent.__init__(self)
这种方式与super(Child, self).__init__()有区别么?博客中特别推荐了《Python’s super() considered super!》这篇文章,因此博主翻译了一下,以下是正文,供大家一起交流。有翻译不到位之处,不吝指正。

1. 前言

如果你对python的内建函数—super()的威力并不惊叹,那你可能也不太清楚它到底能够做什么,或是如何高效地使用它。虽然有好多关于super()的阐述,但是其中也不乏失败的文章。本文希望能通过以下几点中寻求突破:举出一些实用的例子;提供一个清晰形象的模型,介绍super()是如何工作的;展示细腻的技术指南,确保每次都能使super()工作;在使用super()建立新类时,给出具体的建议;为经典的ABCD类的多继承例图,提供有帮助的案例。以下例子是使用python3语法写的,当然读者也可以通过super()with python2.7 查看python2.x语法的例子。一个派生类继承了一个内建类dict,并重写了__setitem__方法:
class LoggingDict(dict):
def __setitem__(self, key, value):
logging.info('Settingto %r' % (key, value))
super().__setitem__(key, value)
LoggingDict类拥有和内建类dict相同的“能力”,但是LoggingDict重写了__setitem__方法,一旦LoggingDict对象中的一个键值对(entry)有了改变(新建,修改),都会将这个键值对打印出来。打印出键值对之后,__setitem__方法使用super()来负责实际的内部字典更新的工作。在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()指向的间接引用确定了,我们也可以轻易地通过影响一个值(calculation),使它指向其他的一些类。这个值(calculation)不但依赖super需要调用的类,而且依赖实例对象的祖先们,在这里我们称他们为“祖先树”(其实并不是“树”的数据结构)。第一部分,super调用的类是由该类的代码决定的,例如在上述的第一段代码中super()调用的是dict.__setitem__方法。如果源码确定,那么这部分是确定的。而第二部分将会更加有趣,因为实例的祖先们的顺序往往是多变的。现在我们利用上述例子中的LoggingDict类和collections.OrderedDict类(它也是继承dict类),创建一个可以当键值对更新时打印log,并且有序的字典类LoggingOD:
class LoggingOD(LoggingDict, collections.OrderedDict):
pass
目前,新建的类的祖先树是:LoggingOD, LoggingDict, OrderedDict, dict, object。在这里,一个重要的结论就是: super()调用时,OrderedDict的搜索顺序是排在LoggingDict之后,而在dict之前的!这就意味着,当LoggingOD类对象的键值对更新时,会在LoggingDict.__setitem__接口中调用super(),此时就会先调用OrderedDict.__setitem__,而并非调用 dict 的。仔细想一下吧。我们并没有修改LoggingDict类的源码,而只是创建了一个 继承了两个现有类(LoggingDict类和OrderedDict类)的子类-LoggingOD类,以此就可以控制super()的搜寻的顺序了。

2. 搜索顺序

我们说的“搜索顺序”或是“祖先树”的词语,更官方的叫法就是“方法解析顺序”(MRO,Method Resolution Order)。如果想知道某个类的MRO(方法解析顺序,下同),使用属性__mro__即可,例如:
>>> print(LoggingOD.__mro__)
(<class '__main__.LoggingOD'>,
<class '__main__.LoggingDict'>,
<class 'collections.OrderedDict'>,
<class 'dict'>,
<class 'object'>)
如果我们使用MRO来创建一个子类,那就必须知道MRO是怎么计算得到的。其实规则也很简单。这些类的搜索顺序(search order)包括子类,该子类的父类,以及父类的父类等等,直到搜索到所有类的根父类 object为止。子类的搜索顺序要先与它的父类,如果他有多个父类,那么就按照继承基类(它们是一个元组)的先后顺序。所以上述的顺序可以通过以下几个约束得出:LoggingOD 先于它的父类,LoggingDict和OrderedDictLoggingDict先于OrderedDict,因为LoggingDict.__bases__是元组(LoggingDict,OrderedDict)LoggingDict先于它的父类dictOrderedDict先于它的父类dictdict先于它的父类object显然,解决这个问题的复杂度是线性的。有很多非常好的文献讲述了这个问题(请看下面的备注和文献),但是新建一个子类,并获得MRO只需要记住两点即可:1.子类的搜索顺序要先于父类;2. 父类们的搜索顺序准从子类的__bases__ 的顺序。

3. 实践建议

super()其实是用来调用在实例对象的“祖先树”中其他类的一个委托函数。类必须“通力合作”来保证这个可以重排顺序方法super()的调用。这样就轻易地引入三个实际的问题。通过MRO,super()要调用的方法必须要存在;调用者和被调用者的函数签名必须一致;在依据MRO得到的调用方法链上,每一处都需要调用super()1)首先,让我们来看一下匹配调用者和被调用者的函数签名的策略。相比于事先已经知晓的被调用函数,在不知道被调用函数的情况下的匹配操作是比较有难度的。而使用super()时,在新建函数时候也并不知道被调用函数的签名,因为后面写的子类往往会在MRO中引入新的类。'''这段话可能令人费解,博主举个上述LoggingOD那个例子,我简述一下他想表达的意思:创建了LoggingOD类,在MRO中引入了LoggingDict类和collections.OrderedDict类,顺序是:LoggingOD->LoggingDict->collections.OrderedDict,也就是说,LoggingDict类中的__setitem__接口调用super()的函数签名要与collections.OrderedDict的__setitem__的签名一致,但是我们在创建LoggingDict类时,根本不会去注意在__setitem__中的super调用者要匹配被调用者collections.OrderedDict的__setitem__接口的签名。这才是导致困难的原因。'''一个解决方法是严格遵守固定的函数签名,包括参数个数和顺序。这个方法在上述的例子(重写__setitem__时,固定两个参数,key和value)会运行的很好。因为这样LoggingOD的__setitem__的函数签名会与LoggingDict,collections.OrderedDict,dict保持一致。另一个更加灵活的方法是需要MRO中的类们通力合作:把函数参数写成"形参=实参"的字典方式进行传递,剥离掉当前类需要的参数,并将剩下的参数以不定项参数形式 **kwds传到MRO的下一个类中。最后调到MRO末端时,"形参=实参"字典方式的参数对便为空了,这是因为MRO末端的类是不需要任何参数的,例如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()调用的MRO中的一系列类方法都是存在的,而不会抛出AttributeError的异常。
对于那些object没有的方法,比如ColoredShape.draw()方法,我们就需要写一个root类来保证在object类之前能够调用。root类的职责“吃”进某个方法(比如draw()),且不再使用super()向后调用。
Root.draw方法也要负责做一些容错处理,例如使用断言(assert)来确保MRO链中root类后的类(比如object类)没有draw方法。如果某个子类“阴差阳错”地继承了某个父类,这个父类正好也有draw()方法,而不继承root类,这个这种容错处理是有必要的。
class Root:def draw(self):# the delegation chain stops hereassert not hasattr(super(), 'draw')class Shape(Root):def __init__(self, shapename, **kwds):self.shapename = shapenamesuper().__init__(**kwds)def draw(self):print('Drawing.  Setting shape to:', self.shapename)super().draw()class ColoredShape(Shape):def __init__(self, color, **kwds):self.color = colorsuper().__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()方法。因为若不这样的话,子类调用draw中的super()方法,等在MRO上轮到这些类时,不能往后转到root.draw(),并以此终结。因此当我们写了一些新的继承某些类的子类时,就需要写清楚这个子类的root类是哪个。这个条件和python要求每一个新建的异常类都要继承BaseException类,并没多大区别。3)前面两点讲了保证super()调用的方法必须存在,而且函数签名要一致。但是,我们还是要保证这条MRO调用链的每一环都要调用super(),才得以不断,一直调用到root类。这个相对前两点比较简单,只要在MRO链的每一环的相应位置加入super()方法即可。上述列出的三点实践经验有助于你设计出合理的,紧凑的新类。

4. 如何与“不理想”类合作

有时候,我们想要创建一个子类,继承自第三方库中的某些类,但是这些第三方库类并非为这个子类而设计的,或因在目标的方法中没有调用super()方法,或因该类没有继承这个子类的root类,而导致“不理想”。这种情况可以通过适配类(adapter class ) 来桥接这个子类与“不理想”类。比如,下文中的 Moveable类,它的所有接口都没有调用super(),存在__init__方法,但是并不是与object.__init__兼容(因为他不继承object类,是旧式类),当然它也没有继承root类:
class Moveable:def __init__(self, x, y):self.x = xself.y = ydef draw(self):print('Drawing at position:', self.x, self.y)
如果我们想要使用这个Moveable类,并把它放入ColoredShape类的MRO中,我们就可以创建一个适配类MoveableAdapter来准守使用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):passMovableColoredShape(color='red', shapename='triangle',x=10, y=20).draw()

5. 完整例子-开心就好 # 博主认为与本文关系不大:)

在python2.7和3.2中,collections模块有一个Counter类和OrderedDict类,这两个类可以融合成为一个OrderedCounter类:
from collections import Counter, OrderedDictclass 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')

6. 备注和文献

1. 当创建一个继承内建类的类时,例如dict,那么与此同时,就往往要重写或是扩展dict中的其他的多个方法。在上述例子中, LoggingDict.__setitem__并没有被LoggingDict的其他方法使用,例如LoggingDict.update。所以这也必须重写LoggingDict.update这个接口。这个要求对super()并不是独一无二的,而是,一旦这些内建类被继承了,这个要求就必须达到。2. 如果某个类的实例的MRO中,某个类的顺序要先于另一个类,那么可以添加一句断言来检验MRO的顺序,也可以以此作为对该类的一种书面说明形式。例如,LoggingOD类的MRO,是LoggingOD->LoggingDict->OrderedDict->dict这样的顺序,则可以这样断言:[/code]
position = LoggingOD.__mro__.indexassert position(LoggingDict) < position(OrderedDict)assert position(OrderedDict) < position(dict)
3. MRO的线性求解方法文档:python MRO官方文档维基百科。4. Dylan编程语言(Dylan programming language)有一个 next-method 的构造函数,它的运行机理和python的super()类似。可以简要看一下 (Dylan’s class docs )5. 上述例子的源码请参考Recipe 577720,它均采用python3的语法写的,python2.x的super()与python3的有些不一样,它是把super()方法内的参数列表显式的表达出来,比如super(obj, type),而且python2.x的super()函数只使用与新式类(new-style class,显式继承与object或是其他内建类型)的情况。上述例子的python2.x的源码请移步 Recipe 577721
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: