Search code examples
pythonsuperdiamond-problem

Why does super() inherits the "wrong" class?


I am looking at the diamond problem and got a question:


class A:
    def __init__(self):
        print("This is class A")

class B(A):
    def __init__(self):
        print("This is class B")
        super().__init__()

class C(A):
    def __init__(self):
        print("This is class C")
        super().__init__()

class D(B, C):
    def __init__(self):
        print("This is class D")
        super().__init__()


i = D()

This is class D
This is class B
This is class C
This is class A

It works as intended and that's nice, but i would like to know why the super().__init__() in class B doesn't go to class A and instead C is called.

If a class has a super() and it inherits from a parent class, it should go there.

If i remove it on B the code won't get to C nor A.

I know of the MRO and how it is actually going as expected:

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

But i don't know why.

It's very weird than the non-super() implementation of this code has the same MRO yet A is printed twice:


class A:
    def __init__(self):
        print("This is class A")

class B(A):
    def __init__(self):
        print("This is class B")
        A.__init__(self)

class C(A):
    def __init__(self):
        print("This is class C")
        A.__init__(self)

class D(B, C):
    def __init__(self):
        print("This is class D")
        B.__init__(self)
        C.__init__(self)


i = D()
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

Here is the opposite, i know the MRO is correct, but it's weird the actual execution doesn't go that way:

This is class D
This is class B
This is class A
This is class C
This is class A

I would like to know what is the logic behind super() behavior.

When asking this around the web pretty much everyone links me to this: https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ but i really don't get it, his explanation seems way too technical and his examples (the few ones i understood) far more complex than they should be to explain the point...which is why i would like a...simpler explanation.

  • Super() has to follow the MRO even if the inheritance on the parent class would suggest otherwise?

  • Super() is unable to go to the parent class of a parent class and therefore if there is a super in a parent class it will go instead to the second inherited class?

  • Also, kind of unrelated but how common is to see the diamond problem in a real, workplace enviroment? Seems like a very convoluted way to work.


Solution

  • You need to keep in mind that the MRO is not just a simple follow-the-leader. It creates a graph-like structure and resolves identical inheritances to avoid duplication. In your first example, you have created a diamond inheritance.

       A
      / \
     B   C
      \ /
       D
    

    The MRO seeks resolution of methods from the first level up (the immediate parent classes), from left to right (B then C), then the next level up, from left to right (just A here), and so on.

    In this case, because you have both B and C inheriting from A, the top level resolves to a single A and creates the diamond diagram above.

    Let's look at your second example:

    class D(B, C):
        def __init__(self):
            print("This is class D")
            B.__init__(self)
            C.__init__(self)
    

    By implementing it this way, you are effectively by-passing the MRO. You have taken the inheritance diamond and made it an inheritance olive fork that looks like this:

     A   A
     |   |
     B   C
      \ /
       D
    

    Because of this you are calling the initialization of A twice, which does not need to occur. In long inheritance chains or complicated initialization routines, this is very inefficient.