super() and multiple inheritance
Lets consider a complicated diamond multiple inheritance scenario.
hint: multiple inheritance is not trivial to implement, and this example will show super() is not a silver bullt
class A:
def __init__(self):
print("A")
class B:
def __init__(self):
print("B")
class C(A):
def __init__(self, arg):
print("C","arg=",arg)
A.__init__(self)
class D(B):
def __init__(self, arg):
print("D", "arg=",arg)
B.__init__(self)
class E(C,D):
def __init__(self, arg):
print("E", "arg=",arg)
C.__init__(self, arg)
D.__init__(self, arg)
E(10)
we want to rewrite this example using super()
super() actually returns a proxy object that understands the MRO of the object
and will call the next function in the hierarchy, like so:
class A:
def __init__(self):
print("A")
super().__init__()
class B(object):
def __init__(self):
print("B")
super().__init__()
class C(A):
def __init__(self, arg):
print("C","arg=",arg)
super().__init__()
class D(B):
def __init__(self, arg):
print("D", "arg=",arg)
super().__init__()
class E(C,D):
def __init__(self, arg):
print("E", "arg=",arg)
super().__init__(arg)
print("MRO:", [x.__name__ for x in E.__mro__])
E(10) # this won't work
What’s happening here?
<ipython-input-32-7c928c6c4b55> in __init__(self)
2 def __init__(self):
3 print("A")
----> 4 super().__init__()
5
6 class B(object):
TypeError: __init__() missing 1 required positional argument: 'arg'
Look at the MRO:
MRO: ['E', 'C', 'A', 'D', 'B', 'object']
looks like A’s __init__
function is calling D’s __init__
function, even though A does not inherit from D … this makes sense since we want to make sure that all the __init__
functions in the hierarchy are being called exactly once …
super does not call your superclass. You must be prepared to call any other class’s method in the hierarchy and be prepared to be called from any other class’s method.
but how do we solve the issue?
using super() in a multiple inheritance setting
We need to keep to principles in mind:
-
super()
usage has to be consistent: In a class hierarchy, super should be used everywhere or nowhere. is part of the contract of the class. if one classes usessuper()
all the classes MUST also usesuper()
in the same way, or otherwise we might call certain functions in the hierarchy zero times, or more than once -
to correctly support
__init__
functions with any parameters, the top-level classes in your hierarchy must inherit from a custom class like SuperObject:# module superobject in this repository class SuperObject: def __init__(self, **kwargs): mro = type(self).__mro__ assert mro[-1] is object if mro[-2] is not SuperObject: raise TypeError( 'all top-level classes in this hierarchy must inherit from SuperObject', 'the last class in the MRO should be SuperObject', f'mro={[cls.__name__ for cls in mro]}' ) # super().__init__ is guaranteed to be object.__init__ init = super().__init__ init() def super_call(self, super_, funcname, **kwargs): """ cooperatively calls a function on super. usage: self.super_call(super(), 'my_method_name', param1='example', param2='whatever') """ super_func = getattr(super_, funcname, None) if super_func is not None: return super_func(**kwargs)
i’ve added SuperObject to a module in this git repository you can use SuperObject in your own code by importing it:
from superobject import SuperObject
-
when calling functions on
super()
make sure you take into account the fact thatclass object
may not have that function and therefore the call might fail. avoid this by using a function likeSuperObject.super_call()
-
if a overriden functions in the class hierarchy can take differing arguments, always pass all arguments you received on to the super function as keyword arguments, and, always accept **kwargs.
For more details, see my writeup super() and changing the signature of cooperative methods
Example rewritten to support these principles
from superobject import SuperObject
class A(SuperObject):
def __init__(self, **kwargs):
print("A")
super(A, self).__init__(**kwargs)
def test(self, param1, **kwargs):
self.super_call(super(), 'test', param1=param1, **kwargs)
print("A", 'test', f"param1={param1}")
class B(SuperObject):
def __init__(self, **kwargs):
print("B")
super(B, self).__init__(**kwargs)
def test(self, param2, **kwargs):
self.super_call(super(), 'test', param2=param2, **kwargs)
print("B", 'test', f"param2={param2}")
class C(A):
def __init__(self, age, **kwargs):
print("C",f"age={age}")
super(C, self).__init__(age=age, **kwargs)
def test(self, param1, param3, **kwargs):
self.super_call(super(), 'test', param1=param1, param3=param3, **kwargs)
print("C", 'test', f"param1={param1}", f"param3={param3}")
class D(B):
def __init__(self, name, **kwargs):
print("D", f"name={name}")
super(D, self).__init__(name=name, **kwargs)
def test(self, param2, param4, **kwargs):
self.super_call(super(), 'test', param2=param2, param4=param4, **kwargs)
print("D", 'test', f"param2={param2}", f"param4={param4}")
class E(C,D):
def __init__(self, name, age, *args, **kwargs):
print( "E", f"name={name}", f"age={age}")
super(E, self).__init__(name=name, age=age, *args, **kwargs)
e = E(name='python', age=28)
print()
e.test(param1='p1', param2='p2', param3='p3', param4='p4')