python: super
many people will tell you super()
is a nifty tool in python; it
allows you to call a method in super class without specifying super class name:
## example 0
class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
super().__init__() # instead of `A.__init__(self)`
print('B')
B()
output:
A
B
this allows you to change super class name while using the same code at call site;
however, if you think super()
means to return the super class, you are
horribly wrong; the problem manifests itself when you start programming with
multiple inheritance;
a simple counter example is to extend the above example like this:
## example 1
class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
super().__init__()
print('B')
class C(A):
def __init__(self):
print('C')
class D(B, C): pass
D()
output:
C
B
now you can see super()
in B
doesnt direct to A
but C
;
demystify super
in python, each class has a method resolution order; it defines the order
of method search; for example, in example 1, the mro of class D
is DBCA
; the
class next to B
is C
; that is why super()
in B
refers to C
(not A
);
in fact, the signature of super()
is super(class, mro_class)
, whose exact
semantic is:
- return a proxy object that delegates method calls to the next class of
class
in the mro ofmro_class
;
for example, if D
extends C
extends B
extends A
, then D
has mro
DCBA
, so super(C, D)
returns B
; note that, for sake of brevity, in this
article we do not distinguish the returned proxy object and its proxied object;
you should know super(C, D)
is a proxy of B
, not B
itself;
call signatures
the classic form of super()
is super(class, mro_class)
; we recommend you to
use this form whenever possible; in addition, super()
also has several other
call signatures:
-
super(class, mro_obj)
:super()
allows to use a non-type objectmro_obj
as the second parameter; in this form, the mro is inferred from the class ofmro_obj
; plus, proxied method calls are bound tomro_obj
; -
super(class)
:super()
allows to omit the second parameter; in this form, the returned proxy object is an unbound super; this is very tricky and it is recommended not to use this form at all; -
super()
:super()
allows to omit both parameters when used inside class definition; in this form, the compiler fills in the missing parameters:class C(B): def method(self, arg): super().method(arg) # This does the same thing as: # super(C, self).method(arg)
solution
now back to our problematic example 1; the problem happens because:
-
when we call
super().__init__()
inB.__init__
, we mean to callA.__init__
; -
however, in our multiple inheritance heterarchy,
super()
, which is equivalent tosuper(B, self)
, works like (a bound version of)super(B, D)
(becauseself
is an instance ofD
), and resolves toC
(becauseD
has mroDBCA
);
to fix this, we need to pass super()
the correct mro (B
, not D
):
## example 2
class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
super(B, B).__init__(self) # super(B, B) not super(B, D)
print('B')
class C(A):
def __init__(self):
print('C')
class D(B, C): pass
D()
output:
A
B
conclusion
-
if you use
super()
to indirectly name super class, you should always use its two-argument formsuper(class, mro_class)
with both classes set to the current class (likesuper(X, X)
); this limits method resolution to the class itself and its super classes (a property which i call forward secrecy);within methods, you can also use
super(__class__, __class__)
to avoid explicit class names:class B(A): def __init__(self): super(__class__, __class__).__init__(self) print('B')
__class__
is an implicit closure reference created by the compiler if any methods in a class body refer to either__class__
orsuper
. -
if you use
super()
, in its zero-argument form, to support cooperative multiple inheritance, i suggest you give up this idea unless you really really need it and your classes are only to be used by yourself;the reason is, as you have seen in example 0 and 1, you do not know which class your
super()
resolves to when you are writing classB
; users who extend classB
need to checksuper()
calls are wired correctly across the whole class heterarchy, which can be huge burden and inflexibility; -
unfortunately, python uses
super()
by default, which means if you omit a method override, a default implemention based onsuper()
is assumed:class B(A): pass
works like:
class B(A): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
this effectively passes the method call to the next candidate in the object mro, producing a no-op effect; this is nifty with the assumed cooperative multiple inheritance paradiam, but is at a discrepancy from the idea that each class only cares about itself and its own superclasses;
combining all these points, i came to the conclusion that super()
-based
cooperative multiple inheritance in python is an anti-pattern; the idea of
cooperative multiple inheritance (broad sight) and the idea of forward secrecy
(narrowed sight) cannot be easily reconciled; furthermore, super()
in python
is a misnomer; if you want to use it correctly, remember this all the time:
super()
means next, not parent;