First of all, the form super()
in Python 3 is really the same thing as super(<CurrentClass>, self)
, where the Python compiler provides enough information for super()
to determine what the correct class to use is. So in E.foo()
, super().foo()
can be read as super(E, self).foo()
.
To understand what is going on, you need to look at the class.__mro__
attribute:
This attribute is a tuple of classes that are considered when looking for base classes during method resolution.
It is this tuple that shows you what the C3 Method Resolution Order is for any given class hierarchy. For your class E
, that order is:
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
>>> for cls in E.__mro__: # print out just the names, for easier readability.
... print(cls.__name__)
...
E
D
B
C
A
object
The super()
object bases everything off from that ordered sequence of classes. The call
super(SomeClass, self).foo()
results in the following series of steps:
- The
super()
object retrieves the self.__mro__
tuple.
super()
locates the index for the SomeClass
class in that tuple.
- Accessing the
foo
attribute on the super()
object triggers a search for a class that has a foo
attribute on the MRO, starting at the next index after the SomeClass
index.
- If the attribute found this way is a descriptor object binds the attribute found this way to
self
. Functions are descriptors, binding produces a bound method, and this is how Python passes in the self
reference when you call a method.
Expressed as simplified Python code that ignores edge cases and other uses for super()
, that would look like:
class Super:
def __init__(self, type_, obj_or_type):
self.mro = obj_or_type.__mro__
self.idx = self.mro.index(type_) + 1
self.obj_or_type = obj_or_type
def __getattr__(self, name):
for cls in self.mro[self.idx:]:
attrs = vars(cls)
if name in attrs:
result = attrs[name]
if hasattr(result, '__get__'):
result = result.__get__(obj_or_type, type(self.obj_or_type))
return result
raise AttributeError(name)
Combining those two pieces of information, you can see what happens when you call e.foo()
:
print('foo in E')
is executed, resulting in foo in E
super().foo()
is executed, effectively the same thing as super(E, self).foo()
.
- The MRO is searched, starting at the next index past
E
, so at D
(no foo
attribute), moving on to B
(no foo
attribute), then C
(attribute found). C.foo
is returned, bound to self
.
C.foo(self)
is called, resulting in foo fo C
super(B, self).foo()
is executed.
- The MRO is searched, starting at the next index past
B
, so at C
(attribute found). C.foo
is returned, bound to self
.
C.foo(self)
is called, resulting in foo fo C
super(C, self).foo()
is executed.
- The MRO is searched, starting at the next index past
C
, so at A
(attribute found). A.foo
is returned, bound to self
.
A.foo(self)
is called, resulting in foo of A