pythonmultiple-inheritancesupermethod-resolution-order

How is super() able to (apparently) resolve to, and call, multiple methods?


I'm working on some Python code that seems like it'll be a good fit for multiple inheritance, and I'm reading about multiple inheritance and super() to make sure I correctly understand how they work.

My understanding of super() is that - to paraphrase - it walks a class's method resolution order and returns a "dispatcher" of sorts, where accessing a method or attribute via __getattr__ accesses the "first" implementation of that method or attribute, as dictated by the MRO.

However, this example from RealPython has me baffled. (I've added print statements, and I've edited their example to remove all the functional methods, since all I care about here is how super() works with the MRO and the various __init__ methods.)

class Rectangle:
    def __init__(self, length, width, **kwargs):
        print("Running Rectangle.__init__")
        self.length = length
        self.width = width
        super().__init__(**kwargs)

class Square(Rectangle):
    def __init__(self, length, **kwargs):
        print("Running Square.__init__")
        super().__init__(length=length, width=length, **kwargs)

class Triangle:
    def __init__(self, base, height, **kwargs):
        print("Running Triangle.__init__")
        self.base = base
        self.height = height
        super().__init__(**kwargs)

class RightPyramid(Square, Triangle):
    def __init__(self, base, slant_height, **kwargs):
        print("Running RightPyramid.__init__")
        self.base = base
        self.slant_height = slant_height
        kwargs["height"] = slant_height
        kwargs["length"] = base
        super().__init__(base=base, **kwargs)

_ = RightPyramid(10, 5)

My expectation is that because Square comes before Triangle in RightPyramid's inheritance order, super(RightPyramid).__init__ is equivalent to Square.__init__. And indeed:

>>> super(RightPyramid).__init__
<bound method Square.__init__ of <__main__.RightPyramid object at [...]>

However, when I actually run this code to look at what gets printed:

lux@parabolica:~/Desktop $ python test.py
Running RightPyramid.__init__  # No problem
Running Square.__init__  # Square comes first, makes sense
Running Rectangle.__init__  # Square calls Rectangle, gotcha
Running Triangle.__init__  # What? How?!

What's going on here? How is Triangle.__init__ able to get called?

The funny thing is, this is actually exactly what I want to have happen in the code I'm working on - "mixing together" multiple __init__ methods from more than one superclass - but none of the documentation or articles I've read indicate that super() is supposed to behave this way. As far as I've read, super() should only "resolve to", and call, one single method; from examining super(RightPyramid).__init__, it seems like that's what's happening, but from the output of the code, Triangle.__init__ is clearly getting called... somehow.

What am I misunderstanding here, and where can I read more about this functionality of super()?

(This seems to be what the official documentation for super() is referring to in the paragraph starting with "The second use case is to support cooperative multiple inheritance...", but it doesn't go on to say anything more specific about what that means, how that works, or provide any examples for that use-case.)


Solution

  • super().<method>() calls the method from the next class in the MRO, it doesn't call the methods in all the superclasses automatically. The entire MRO chain will only be processed if each of these methods also calls super().<method>().

    This is why it's called cooperative multiple inheritance -- it depends on each class cooperating by also calling super().