1

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.

PPS
  • 69
  • 1
  • 8
  • 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 '18 at 03:05
  • 1
    @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 '18 at 03:06
  • Thank you @abarnert, I appreciate your insights. Do you happen to know where I could read more about this cooperative multiple inheritance mixins, and their use cases? – Reblochon Masque Aug 25 '18 at 04:11
  • 1
    @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](https://www.artima.com/weblogs/viewpost.jsp?thread=281127), and anyone explaining mixins, like [this answer](https://stackoverflow.com/questions/533631/), 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 '18 at 04:24
  • 1
    @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 '18 at 04:26
  • 1
    @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`](https://github.com/pallets/werkzeug/blob/master/werkzeug/wrappers.py#L1639), 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 '18 at 04:28
  • Thank you both, I fixed the order of the classes and the indentation. Still wrapping my head around all your inputs, but I really appreciate it. – PPS Aug 25 '18 at 07:19

1 Answers1

4

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, 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:

>>> [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:

>>> 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:

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

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.


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.


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.

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.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 1
    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. – PPS Aug 25 '18 at 07:57