python的super详解(一)

如果你没有被Python的super()惊愕过,那么要么是你不了解它的威力,要么就是你不知道如何高效地使用它。

有许多介绍super()的文章,这一篇与其它文章的不同之处在于:

  • 提供了实例
  • 阐述了它的工作模型
  • 展示了任何场景都能使用它的手段
  • 有关使用super()的类的具体建议
  • 基于抽象ABCD钻石模型的实例

下面是一个使用Python 3语法,扩展了builtin类型dict中方法的子类:

 
  1. import pprint

  2. import logging

  3. import collections

  4.  
  5. class LoggingDict(dict):

  6.  
  7. def __setitem__(self, key, value):

  8. logging.info('Setting %r to %r' % (key, value))

  9. super().__setitem__(key, value)

LoggingDict继承了父类dict的所有特性,同时其扩展了__setitem__方法来记录被设置的key;在记录日志之后,该方法用super()将真正的更新操作代理给其父类。

我们可以使用dict.__setitem__(self, key, value)来完成super()的功能,但是super()更优,因为它是一个计算出来的间接引用。

间接的一个好处是,我们不需要使用名字来指定代理类。如果你将基类换成其它映射(mapping)类型,super()引用将会自动调整。你只需要一份代码:

 
  1. class LoggingDict(someOtherMapping): # 新的基类

  2.  
  3. def __setitem__(self, key, value):

  4. logging.info('Setting %r to %r' % (key, value))

  5. super().__setitem__(key, value) # 无需改变

对于计算出的间接引用,其除了隔离变化外,依赖于Python的动态性,可以在运行时改变其指向的class。

计算取决于类在何处被调用以及实例的继承树;super在何处调用取决于类的源码,在上例中super()是在LoggingDict.__setitem__方法中被调用的;实例的继承树在后文详述。

下面先构造一个有序的logging字典:

 
  1. class LoggingOD(LoggingDict, collections.OrderedDict):

  2. pass

新class的继承树是:LoggingODLoggingDictOrderedDictdictobject。出人意料的是OrderedDict竟然介于LoggingDict之后和dict之前,这意味着LoggingDict.__setitem__super()调用会将键/值的更新委托给OrderedDict而不是dict

在上例中,我们并没有修改LoggingDict的源码,只是创建了一个子类,这个子类的唯一逻辑是组合两个已有的类并控制它们的搜索顺序(search order)。

Search Order

上面提到的搜索顺序或者继承树的官方称谓是Method Resolution Order(方法解析顺序)即MRO。可以用__mro__属性方便地打印出对象的MRO:

pprint.pprint(LoggingOD.__mro__)
 
  1. (<class '__main__.LoggingOD'>,

  2. <class '__main__.LoggingDict'>,

  3. <class 'collections.OrderedDict'>,

  4. <class 'dict'>,

  5. <class 'object'>)

如果我们想创建出其MRO符合我们意愿的子类,就必须知道它是如何计算的。MRO的计算很简单,MRO序列包含类、类的基类以及基类们的基类......这个过程持续到到达objectobject是所有类的根类;这个序列中,子类总是出现在其父类之前,如果一个子类有多个父类,父类按照子类定义中的基类元组的顺序排列。

上例中MRO是按照这些约束计算出来的:

  • LoggingOD在其父类LoggingDict, OrderedDict之前
  • LoggingDict在OrderedDict之前是因为LoggingOD.bases是(LoggingDict, OrderedDict)
  • LoggingDict在它的父类dict之前
  • OrderedDict在它的父类dict之前
  • dict在它的父类object之前

解析这些约束的过程称为线性化(linearization)。创建出MRO符合我们期望的子类只需知道两个约束:子类在父类之前;符合__bases__里的顺序。

Practical Advice

super()用来将方法调用委托给其继承树中的一些类。为了让super能正常作用,类需要协同设计。下面是三条简单的解决实践:

  • 被调用的super()需存在
  • 调用者和被调用者的参数签名需匹配
  • 方法的任何出现都需要使用super()

1) 先来看一下使调用者和被调者参数签名匹配的策略。对于一般的方法调用而言,被调者在被调之前其信息是已经获知的;然而对于super(),直到运行时才能确定被调者(因为后面定义的子类可能会在MRO中引入新的类)。

一种方法是使用positional参数固定签名。这种方法对于像__setitem__这种只有两个参数的固定签名是适用的。LoggingDict例子中__setitem__的签名和dict中一致。

另一种更灵活的方法是规约继承树中的每一个方法都被设计成接受keyword参数和一个keyword参数字典,“截留住”自身需要的参数,然后将剩下的参数使用**kwds转发至父类中的方法,使得调用链中的最后一次调用中参数字典为空(即沿着继承树一层一层地将参数剥离,每层都留下自己需要的,将余下的参数传递给基类)。

每一层都会剥离其所需的参数,这样就能保证最终将空字典传递给不需要参数的方法(比如,object.__init__不需要参数):

 
  1. class Shape:

  2.  
  3. def __init__(self, shapename, **kwds):

  4. self.shapename = shapename

  5. super().__init__(**kwds)

  6.  
  7.  
  8. class ColoredShape(Shape):

  9.  
  10. def __init__(self, color, **kwargs):

  11. self.color = color

  12. super().__init__(**kwargs)

  13.  
  14. cs = ColoredShape(color='red', shapename='circle')

2) 现在来看一下如何保证目标方法存在。

上例仅展示了最简单的情形。我们知道object有一个__init__方法,它也总是MRO链中最后一个类,因此任意数量的super().__init__最终都会以调用object.__init__结束。换言之,我们可以保证调用继承树上任意对象的super().__init__方法都不会以产生AttributeError而失败。

对于object没有的方法(比如draw()方法),我们需要写一个根类并保证它在object对象之前被调用。根类的作用仅仅是将方法调用“截住”而不会再进一步调用super()。

Root.draw也可以使用defensive programming策略使用断言来保证draw()方法不会再被调用。

 
  1. class Root:

  2.  
  3. def draw(self):

  4. #: 代理调用链止于此

  5. assert not hasattr(super(), 'draw')

  6.  
  7.  
  8. class Shape(Root):

  9.  
  10. def __init__(self, shapename, **kwds):

  11. self.shapename = shapename

  12. super().__init__(**kwds)

  13.  
  14. def draw(self):

  15. print('Drawing. Setting shape to:', self.shapename)

  16. super().draw()

  17.  
  18.  
  19. class ColoredShape(Shape):

  20.  
  21. def __init__(self, color, **kwds):

  22. self.color = color

  23. super().__init__(**kwds)

  24.  
  25. def draw(self):

  26. print('Drawing. Setting color to:', self.color)

  27. super().draw()

  28.  
  29. cs = ColoredShape(color='blue', shapename='square')

  30. cs.draw()

 
  1. Drawing. Setting color to: blue

  2. Drawing. Setting shape to: square

如果子类想在MRO中注入其它类,那么这些类也需要继承自Root,这样在继承路径上的任何类调用draw()方法都不会最终代理至object而抛出AttributeError。这一规定需在文档中明确,这样别人在写新类的时候才知道需要继承Root。这一约束与Python中要求所有异常必须继承自BaseException并无不同。

3) 上面讨论的两点保证了方法的存在以及签名的正确,然而我们还必须保证在代理链上的每一步中都super()都被调用。这一目标很容易达成,只需要协同设计每一个相关类——在代理链上的每一步中增加一个supper()

How to Incorporate a Non-cooperative Class

在某些场景下,子类可能希望使用多重继承,其大部分父类都是协同设计的,同时也需要继承自一个第三方类(可能将要使用的方法没有使用super或者该类没有继承自根类)。这种情形很容易通过使用adapter class来解决。

例如,下面的Moveable类没有调用super(),init()函数签名与object.init也不兼容,而且它不集成自Root:

 
  1. class Moveable:

  2.  
  3. def __init__(self, x, y):

  4. self.x = x

  5. self.y = y

  6.  
  7. def draw(self):

  8. print('Drawing at position:', self.x, self.y)

如果我们想将这个类与之前协同设计的ColoredShape层级一起使用的话,我们需要创建一个适配器(adapter),它调用了必须的super()方法。

 
  1. class MoveableAdapter(Root):

  2.  
  3. def __init__(self, x, y, **kwds):

  4. self.moveable = Moveable(x, y)

  5. super().__init__(**kwds)

  6.  
  7. def draw(self):

  8. self.moveable.draw()

  9. super().draw()

  10.  
  11.  
  12. class MoveableColoredShape(ColoredShape, MoveableAdapter):

  13. pass

  14.  
  15.  
  16. MoveableColoredShape(color='red', shapename='triangle', x=10, y=20).draw()

 
  1. Drawing. Setting color to: red

  2. Drawing. Setting shape to: triangle

  3. Drawing at position: 10 20

Complete Example - Just for Fun

在Python 2.7和3.2中,collections模块有一个Counter类和一个OrderedDict类,可以将这两个类组合产生一个OrderedCounter类:

 
  1. from collections import Counter, OrderedDict

  2.  
  3.  
  4. class OrderedCounter(Counter, OrderedDict):

  5.  
  6. 'Counter that remembers the order elements are first seen'

  7. def __repr__(self):

  8. return '%s(%r)' % (self.__class__.__name__,

  9. OrderedDict(self))

  10.  
  11. def __reduce__(self):

  12. return self.__class__, (OrderedDict(self),)

  13.  
  14. oc = OrderedCounter('abracadabra')

  15. pprint.pprint(oc)

OrderedCounter(OrderedDict([('a', 5), ('b', 2), ('r', 2), ('c', 1), ('d', 1)]))

Notes and References

  • 当子类化诸如dict()的builtin时,通常需要同时重载或者扩展多个方法。在上面的例子中,_setitem_扩展不能用于诸如dict.update等其它方法,因此可能需要扩展这些方法。这一需求不止限于super(),子类化builtins时都需要。

  • 在多继承中,如果要求父类按照指定顺序(例如,LoggingOD需要LoggingDict在OrderedDict前面,而OrderedDict在dict前面),可以利用断言来验证或者使用文档来表明方法解析顺序:

 
  1. position = LoggingOD.__mro__.index

  2. assert position(LoggingDict) < position(collections.OrderedDict)

  3. assert position(OrderedDict) < position(dict)

参考:

https://github.com/SimonXming/my-blog/issues/9

https://harveyqing.gitbooks.io/python-read-and-write/content/python_advance/python_super_considered_super.html

https://fuhm.net/super-harmful/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值