3

I got the following code from github about MRO and C3, and I don't quite understand the the last three lines, and what the difference among super().foo(), super(B,self).foo() and super(C,self).foo() in python3.x, code as follows:

class A(object):

    def foo(self):
        print('foo of A')

class B(A):
    pass

class C(A):

    def foo(self):
        print('foo fo C')

class D(B, C):
    pass

class E(D):

    def foo(self):
        print('foo in E')
        super().foo()
        super(B, self).foo()
        super(C, self).foo()

if __name__ == '__main__':
    d = D()
    d.foo()
    e = E()
    e.foo()

the expected and actual results is as follow:

foo fo C
foo in E
foo fo C
foo fo C
foo of A    
Andy Kong
  • 63
  • 4

1 Answers1

1

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
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343