Python’s super() considered super!

If you haven't been amazed by Python's super(), then either you don't understand its power, or you don't know how to use it efficiently.

There are many articles introducing super(), this one differs from the others in that:

  • examples are provided
  • Elaborated on its working model
  • Shows how it can be used in any scenario
  • Specific advice on classes using super()
  • An example based on the abstract ABCD diamond model

dictHere's a subclass that extends the methods in the builtin type using Python 3 syntax :

import pprint
import logging
import collections

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

LoggingDictIt inherits all the features of the parent class dict , and at the same time it extends the __setitem__method to record the set key; after logging, the method is used super()to delegate the real update operation to its parent class.

A function we could use dict.__setitem__(self, key, value)to accomplish super(), but super()it's preferable because it's a calculated indirection.

间接One benefit is that we don't need to specify proxy classes by name. If you replace the base class with another mapping type, the super()reference will be adjusted automatically. All you need is a copy of the code:

class LoggingDict(someOtherMapping):                     # 新的基类 def __setitem__(self, key, value): logging.info('Setting %r to %r' % (key, value)) super().__setitem__(key, value) # 无需改变 

For computed indirect references, in addition to isolating changes, relying on Python's dynamics, the class to which it points can be changed at runtime.

The calculation depends on where the class is called and the inheritance tree of the instance; where super is called depends on the source code of the class, which is called super()in the LoggingDict.__setitem__method in the above example; the inheritance tree of the instance is detailed later.

The following first constructs an ordered logging dictionary:

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass 

The inheritance tree for the new class is: LoggingODLoggingDictOrderedDictdictobject. Surprisingly, it OrderedDictturns out to be between LoggingDictafter and dictbefore, which means LoggingDict.__setitem__that the super()invocation delegates key/value updates to OrderedDictnot dict.

In the above example, we didn't modify LoggingDictthe source code, just created a subclass whose only logic is to combine two existing classes and control their search order.

Search Order

The official name for the 搜索顺序or mentioned above is (method resolution order) ie . You can easily print out the MRO of an object with properties:继承树Method Resolution OrderMRO__mro__

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

If we want to create a subclass whose MRO is what we want, we must know how it is calculated. The calculation of MRO is very simple, the MRO sequence contains the class, the base class of the class, and the base class of the base classes... This process continues until it reaches objectthe objectroot class of all classes; in this sequence, subclasses always appear Before its superclass, if a subclass has multiple superclasses, the superclasses are arranged in the order of the base class tuples in the subclass definition.

The MRO in the above example is calculated according to these constraints:

  • LoggingOD is before its parent class LoggingDict, OrderedDict
  • LoggingDict comes before OrderedDict because LoggingOD.bases is (LoggingDict, OrderedDict)
  • LoggingDict precedes its parent class dict
  • OrderedDict precedes its parent class dict
  • dict precedes its parent class object

The process of resolving these constraints is called linearization. To create a subclass whose MRO meets our expectations, we only need to know two constraints: the subclass comes before the superclass; __bases__the order in which it matches.

Practical Advice

super() is used to delegate method calls to some classes in its inheritance tree. In order for super to function properly, classes need to be co-designed. Here are three simple solutions to practice:

  • The called super() needs to exist
  • The parameter signatures of the caller and callee must match
  • Any occurrence of a method requires the use of super()

1) Let's first look at the strategy for matching the caller and callee parameter signatures. For a general method call, the callee's information is known before it is called; however, for super(), the callee cannot be determined until runtime (because subclasses defined later may introduce new information into the MRO) the type).

One way is to use the positional parameter to fix the signature. __setitem__This method is suitable for fixed signatures like this with only two parameters. The signature in the LoggingDictexample is the same as in.__setitem__dict

Another more flexible approach is that each method in the reduction inheritance tree is designed to accept keyword parameters and a dictionary of keyword parameters, "hold back" the parameters it needs, and then forward the remaining parameters **kwdsto the parent class. method, so that the parameter dictionary in the last call in the call chain is empty (that is, the parameters are stripped layer by layer along the inheritance tree, each layer leaves what it needs, and the remaining parameters are passed to the base class).

Each layer strips its required parameters, which guarantees that an empty dictionary ends up being passed to methods that don't need parameters (e.g., object.__init__don't need parameters):

class Shape:

    def __init__(self, shapename, **kwds): self.shapename = shapename super().__init__(**kwds) class ColoredShape(Shape): def __init__(self, color, **kwargs): self.color = color super().__init__(**kwargs) cs = ColoredShape(color='red', shapename='circle') 

2) Now let's see how to ensure that the target method exists.

The above example shows only the simplest case. We know that object has a __init__method, which is also always the last class in the MRO chain, so any number super().__init__will end up in a call object.__init__. In other words, we can guarantee that calling a method on any object in the inheritance tree super().__init__will not fail with an AttributeError .

For methods that object does not have (such as the draw() method), we need to write a root class and ensure that it is called before the object object. The role of the root class is only to "intercept" method calls without further calling super().

Root.draw can also use defensive programming strategies to use assertions to ensure that the draw() method will never be called again.

class Root:

    def draw(self): #: 代理调用链止于此 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() 
Drawing. Setting color to: blue
Drawing. Setting shape to: square

If the subclass wants to inject other classes in the MRO, these classes also need to inherit from Root , so that any class on the inheritance path calling the draw() method will not eventually proxy to the object and throw an AttributeError . This requirement needs to be made clear in the documentation, so that others know that they need to inherit Root when they write new classes . This constraint is not different from the requirement in Python that all exceptions must inherit from BaseException .

3) The two points discussed above ensure that the method exists and the signature is correct, however we must also ensure that super() is called at every step in the proxy chain . This goal is easy to achieve, just need to co-design each related class - add a supper() at each step in the proxy chain

How to Incorporate a Non-cooperative Class

In some scenarios, subclasses may wish to use multiple inheritance, most of their parent classes are co-designed, and also need to inherit from a third-party class (maybe the method to be used is not used superor the class does not inherit from the root class ). This situation is easily solved by using the adapter class .

For example, the Moveable class below does not call super(), the init() function signature is not compatible with object.init , and it is not integrated from 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) 

If we want to use this class with the previously co-designed ColoredShape hierarchy, we need to create an adapter that calls the necessary super() method.

class MoveableAdapter(Root):

    def __init__(self, x, y, **kwds): self.moveable = Moveable(x, y) super().__init__(**kwds) def draw(self): self.moveable.draw() super().draw() class MoveableColoredShape(ColoredShape, MoveableAdapter): pass MoveableColoredShape(color='red', shapename='triangle', x=10, y=20).draw() 
Drawing. Setting color to: red
Drawing. Setting shape to: triangle
Drawing at position: 10 20

Complete Example - Just for Fun

In Python 2.7 and 3.2, collectionsmodules have a Counterclass and a OrderedDictclass, which can be combined to produce a OrderedCounterclass:

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') pprint.pprint(oc) 
OrderedCounter(OrderedDict([('a', 5), ('b', 2), ('r', 2), ('c', 1), ('d', 1)]))

Notes and References

  • When subclassing a builtin such as dict() , it is often necessary to overload or extend multiple methods at the same time. In the above example, the _setitem_ extension cannot be used for other methods such as dict.update , so these methods may need to be extended. This requirement is not limited to super() , it is required when subclassing builtins.

  • In multiple inheritance, if the parent class is required to be in a specified order (for example, LoggingOD requires LoggingDict to precede OrderedDict and OrderedDict to precede dict ), you can use assertions to verify or use documentation to indicate method resolution order:

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326059702&siteId=291194637