#๐Ÿ”’ How is super() able to (apparently) resolve to, and call, multiple methods?

49 messages ยท Page 1 of 1 (latest)

clever sentinel
#

I'm working on some code that seems like will 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 (paraphrasing) it walks a class's MRO 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 absolutely baffled. (I've edited their example to remove all the functional methods and add print statements, 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)

.

naive crownBOT
#

@clever sentinel

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

clever sentinel
#

My expectation is that because Square comes before Triangle in RightPyramid's inheritance order, Square precedes Triangle in RightPyramid's MRO (which is indeed true), and that therefore, super(RightPyramid).__init__ is equivalent to Square.__init__.

However:

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__  # WTF? How?!

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

What's irritating is that 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 - but from this test, that's clearly not true.

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.)

jolly sun
#

make class a b c d, give all a m1 and try this experiment, then give m2 method to only 2 and see the results

#

if method is found it stops checking through the mro

clever sentinel
# jolly sun if it exists it gets called

That doesn't make sense, though, because if I add a breakpoint, it tells me:

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

So it should only be calling Square.__init__, and nothing in the call chain of Square.__init__ ever calls Triangle.__init__. So I don't understand how Triangle.__init__ is being called.

clever sentinel
jolly sun
#

lemme explain u

#
class API1:
  def common_call(self):
    print("call of API1")
    print("...doing stuff API1 should do")

class API2:
  def common_call(self):
    print("call of API2")
    print("...doing stuff API2 should do")

class MergedAPIs(API1, API2):
  def common_call(self):
    super().common_call()
    print("common task executed")```
#

@clever sentinel

#

my bad, i forgot to inherit ๐Ÿ’€

#

if u think that only api1 will get the common call then the api2's call will be missed, its not like that

#

they both get a change because they both have the method

clever sentinel
# jolly sun if u think that only api1 will get the common call then the api2's call will be ...

I mean like... I understand that that happens - but from everything I've read, it doesn't seem like that should happen.

None of the documentation seems to say that super() should "resolve to" more than one method. It should just resolve to one single method. And that's what I get when I examine what's returned by super(RightPyramid).__init__ - I just get a reference to Square.__init__.

And if I do list(super(RightPyramid).__init__), I get a TypeError that methods aren't iterable, which is totally what I'd expect, but it means that super().__getattr__ isn't returning an iterator of methods or something.

Again, I totally agree with you that this definitely happens, but I want to understand why, because nothing makes it sound like this should be happening. And because I want to read more about it, to make sure I'm using it correctly in the code I'm going to write.

jolly sun
#
super(ParentClass, self).__init__```and```py
super().__init__```both are very different things
#

1st one you tell the super that which base class's method u wanna call

#

2nd one u get that from the mro

#

@clever sentinel

#

u got it?

#
class API1:
  def common_call(self):
    print("call of API1")
    print("...doing stuff API1 should do")

class API2:
  def common_call(self):
    print("call of API2")
    print("...doing stuff API2 should do")

class MergedAPIs(API1, API2):
  def common_call(self):
    super().common_call()
    print("common call for all parent classes")
  def common_task_of_api1():
    super(API1, self).common_call()
    print("common call only for API1")
  def common_task_of_api2():
    super(API2, self).common_call()
    print("common call only for API2")```
untold tiger
#

For this, inspecting the mro may be useful.

#

!e ```py
class API1:
pass
class API2:
pass
class MergedAPIs(API1, API2):
pass

print(MergedAPIs.mro())

naive crownBOT
untold tiger
#

That's the order super() will look up functions.

#

So if you use super() in API1 and self is MergedAPIs, it will look at API2 then object

#

!d type.mro

naive crownBOT
#

type.mro()```
This method can be overridden by a metaclass to customize the method resolution order for its instances. It is called at class instantiation, and its result is stored in [`__mro__`](https://docs.python.org/3/reference/datamodel.html#type.__mro__).
untold tiger
#

!d type.mro

naive crownBOT
untold tiger
#

The tuple of classes that are considered when looking for base classes during method resolution.

#

mro stands for method resolution order

#

.rp mro

silver matrixBOT
#

Here are the top 5 results:

Inheritance and Composition: A Python OOP Guide
Supercharge Your Classes With Python super()
Implementing an Interface in Python
Python Logging: A Stroll Through the Source Code
Python Classes: The Power of Object-Oriented Programming
fair nexus
clever sentinel
#

So the thing that's invoking Triangle.__init__ is actually the super() call from Rectangle.__init__?

And that works because even though Triangle isn't a base class of Rectangle, the original caller of super() was RightPyramid, which is a child of Triangle?

fair nexus
#

Yes. You can also see the order in which calls will happen by printing thing.__mro__, ie print(RightPyramid.__mro__) gives (<class '__main__.RightPyramid'>, <class '__main__.Square'>, <class '__main__.Rectangle'>, <class '__main__.Triangle'>, <class 'object'>) which matches my diagram :)

untold tiger
#

I mean, techinically super() doesn't call anything. super has a special getattr that retrieves the next class's attr. So super(A, self).__init__ is the same as B.__init__

clever sentinel
#

Sorry, yeah, that was imprecise

#

But yeah, that makes sense now; I was misunderstanding how super() worked when it was operating "remotely" and didn't realize it would/could ever "go back down" the tree like it does from Rectangle to Triangle.

untold tiger
#

super(current class, object)

#

the compiler will turn super() into ```py
class A:
def foo(self):
class = A
super().foo()

And no args super will inspect the call stack to extract the `__class__` and `self` variables.
#

basically, it's magic'

clever sentinel
#

!close

naive crownBOT
#
Python help channel closed

This help channel has been closed and it's no longer possible to send messages here. If your question wasn't answered, feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.