How MRO, super works in python
How MRO, super works in python
Can someone help me understand how MRO works in python?
Suppose I have four classes - Character, Thief, Agile, Sneaky. Character is the super class to Thief, Agile and Sneaky are siblings. Please see my code and question below
class Character:
def __init__(self, name="", **kwargs):
if not name:
raise ValueError("'name' is required")
self.name = name
for key, value in kwargs.items():
setattr(self, key, value)
class Agile:
agile = True
def __init__(self, agile=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.agile = agile
class Sneaky:
sneaky = True
def __init__(self, sneaky=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sneaky = sneaky
class Thief(Agile, Sneaky, Character):
def pickpocket(self):
return self.sneaky and bool(random.randint(0, 1))
parker = Thief(name="Parker", sneaky=False)
So, here is what I think is going on, please let me know if I'm understanding it correctly.
Since Agile is first on the list, all arguments are first sent to Agile where the arguments will be cross-referenced with the Agile parameters. If there is a match the value will be assigned, then everything that doesn't have a matching keyword will be packed up in *kwargs and sent to the Sneaky class (via super), where the same thing will happen - all arguments get unpacked, cross-referenced with the Sneaky parameters (this is when sneaky = False is set), then packed up in kwargs and sent to Character. Then everything within the Character inint method will run and all values will be set (like the name = "Parker").
HOW I THINK MRO WORKS ON THE WAY BACK
Now that everything made it to the Character class and everything in the Character init method has run, now it has to go back to the Agile and Sneaky classes and finishing running everything in their init methods(or everything under their super). So, it will first go back to the Sneaky class and finish it's init method, then go back to the Agile class and finish the rest of its init method (respectively).
Do I have it confused anywhere? Phew. I'm sorry, I know this is a lot, but I'm really stuck here and I'm trying to get a clear understanding of how MRO works.
Thank you, everyone.
@ReblochonMasque Actually, it makes perfect sense for mixins meant to be used in this kind of cooperative multiple inheritance to call
super
. After all, the only way any method will ever get to Sneaky
is from Agile
calling super
.– abarnert
Aug 25 at 3:06
super
Sneaky
Agile
super
@ReblochonMasque I don't know of anything specific, because it's sort of combining two separate ideas that are usually complicated enough to have their own separate tutorials. Usually anyone explaining cooperative MI with
super
is going to focus examples on diamond hierarchies, like this one, and anyone explaining mixins, like this answer, if they go beyond the basics, is probably going to go on to classes that are simultaneously a mixin and an ABC, like the collections.abc
s…– abarnert
Aug 25 at 4:24
super
collections.abc
@ReblochonMasque But the two concepts fit together fine. A mixin that's intended to be used with a cooperative hierarchy, if it needs to override
__init__
(or anything else), will have to super
it even though it doesn't inherit anything. (Of course that means the mixin can only work with that hierarchy, but that's fine; you're not going to use a werkzeug.AcceptMixin
with non-werkzeug classes even without any super
.– abarnert
Aug 25 at 4:26
__init__
super
werkzeug.AcceptMixin
super
@ReblochonMasque And actually, that just brought up an example: most of the werkzeug mixin methods aren't overriding anything that needs to be chained up, but some are, like
ETagResponseMixin.freeze
, and they use super
to do that. This means ETagResponseMixin
can only be used in a hierarchy where the ultimate base class (or at least some ancestor) has a freeze
method that it can super
, but that's fine, because it's not intended to be used anywhere else.– abarnert
Aug 25 at 4:28
ETagResponseMixin.freeze
super
ETagResponseMixin
freeze
super
1 Answer
1
Your code as posted doesn't even compile, much less run. But, guessing at how it's supposed to work…
Yes, you've got things basically right.
But you should be able to verify this yourself, in two ways. And knowing how to verify it may be even more important than knowing the answer.
First, just print out Thief.mro()
. It should look something like this:
Thief.mro()
[Thief, Agile, Sneaky, Character, object]
And then you can see which classes provide an __init__
method, and therefore how they'll be chained up if everyone just calls super
:
__init__
super
>>> [cls for cls in Thief.mro() if '__init__' in cls.__dict__]
[Agile, Sneaky, Character, object]
And, just to make sure Agile
really does get called first:
Agile
>>> Thief.__init__
<function Agile.__init__>
Second, you can run your code in the debugger and step through the calls.
Or you can just add print
statements at the top and bottom of each one, like this:
print
def __init__(self, agile=True, *args, **kwargs):
print(f'>Agile.__init__(agile=agile, args=args, kwargs=kwargs)')
super().__init__(*args, **kwargs)
self.agile = agile
print(f'<Agile.__init__: agile=agile')
(You could even write a decorator that does this automatically, with a bit of inspect
magic.)
inspect
If you do that, it'll print out something like:
> Agile.__init__(agile=True, args=(), kwargs='name': 'Parker', 'sneaky':False)
> Sneaky.__init__(sneaky=False, args=(), kwargs='name': 'Parker')
> Character.__init__(name='Parker', args=(), kwargs=)
< Character.__init__: name: 'Parker'
< Sneaky.__init__: sneaky: False
< Agile.__init__: agile: True
So, you're right about the order things get called via super
, and the order the stack gets popped on the way back is obviously the exact opposite.
super
But, meanwhile, you've got one detail wrong:
sent to the Sneaky class (via super), where the same thing will happen - all arguments get unpacked, cross-referenced with the Sneaky parameters (this is when sneaky = False is set)
This is where the parameter/local variable sneaky
gets set, but self.sneaky
doesn't get set until after the super
returns. Until then (including during Character.__init__
, and similarly for any other mixins that you choose to throw in after Sneaky
), there is no sneaky
in self.__dict__
, so if anyone were to try to look up self.sneaky
, they'd only be able to find the class attribute—which has the wrong value.
sneaky
self.sneaky
super
Character.__init__
Sneaky
sneaky
self.__dict__
self.sneaky
Which raises another point: What are those class attributes for? If you wanted them to provide default values, you've already got default values on the initializer parameters for that, so they're useless.
If you wanted them to provide values during initialization, then they're potentially wrong, so they're worse than useless. If you need to have a self.sneaky
before calling Character.__init__
, the way to do that is simple: just move self.sneaky = sneaky
up before the super()
call.
self.sneaky
Character.__init__
self.sneaky = sneaky
super()
In fact, that's one of the strengths of Python's "explicit super
" model. In some languages, like C++, constructors are always called automatically, whether from inside out or outside in. Python forcing you to do it explicitly is less convenient, and harder to get wrong—but it means you can choose to do your setup either before or after the base class gets its chance (or, of course, a little of each), which is sometimes useful.
super
Thank you for the detailed explanation. I creating this code while following along with an online python course. I'm not sure why the instructor has me making class attributes and declaring them in the initializer parameters. Hopefully, he explains why in the next few steps. I'll relay that question to their form to see if I can get a good answer. There are a lot of new terms for me in your answer so I have some studying to do. Thanks again.
– Philip Schultz
Aug 25 at 7:57
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
This code doesn't run as posted. There's at least one indentation error, the classes are out of order, … That means it's impossible to debug your code to help answer your questions.
– abarnert
Aug 25 at 3:05