Skip to content

Instantly share code, notes, and snippets.

@moonburnt
Last active May 1, 2023 00:16
Show Gist options
  • Save moonburnt/7bedaea76c73be7745b7bfd60fded786 to your computer and use it in GitHub Desktop.
Save moonburnt/7bedaea76c73be7745b7bfd60fded786 to your computer and use it in GitHub Desktop.
Short introduction guide to multiple inheritance and MRO in python

So, you have a Foo, you have a Bar. Both of them consist of elements that somehow mix together, and you want to make a Baz which inherits from both of them. How would you do that? Well, you'd go for multiple inheritance.

class Foo():
    def get_foo(self):
        return "foo"

class Bar():
    def get_bar(self):
        return "bar"

class Baz(Foo, Bar):
    pass

# Resulting Baz can perform both get_foo() and get_bar()
b = Baz()
b.get_foo()
b.get_bar()

Baz inherits methods from both Foo and Bar and works like a charm doing the job of both parents. Cool, right? But what if we don't live in a perfect world? What if some of the parent's methods overlap with other parent's methods? Lets look at this example:

class Foo():
    def whoami(self):
        print("I am Foo")

class Bar():
    def whoami(self):
        print("I am Bar")

class Baz(Foo, Bar):
    pass

# What will Baz produce?
b = Baz()
b.whoami()

In the example above, Baz().whoami() returned "I am Foo". But why? Well, thats determined by thing called Method Resolution Order, or MRO in short.

Basically, it works like that: if we have a class inheriting from multiple classes and call a method, we look for matching solution from left to right and execute the very first thing we find. If nothing has been found, AttributeError would be thrown. So, in the example above Baz inherits from Foo first - and thus Foo's whoami() overrides the Bar's whoami(). Try to reverse the order - and now it go the opposite way:

class Foo():
    def whoami(self):
        print("I am Foo")

class Bar():
    def whoami(self):
        print("I am Bar")

class Baz(Bar, Foo):
    pass

# Baz returns "I am Bar"
b = Baz()
b.whoami()

Of course, same thing works for variables too:

class Foo():
    me = "I am Foo"

class Bar():
    me = "I am Bar"

class Baz(Foo, Bar):
    pass

# "I am Foo"
b = Baz()
b.me

or for instance-level vars

class Foo():
    def __init__(self):
        me = "I am Foo"

class Bar():
    def __init__(self):
        me = "I am Bar"

class Baz(Foo, Bar):
    pass

b = Baz()
# "I am Foo"
b.me

But what if we want to perform both parent's actions together? Or maybe we would want to specifically call some parent's method instead of another, while keeping the order of rest intact? Well, we can always use parent's methods directly by passing our class'es instance as "self", and thus create a wrapper calling both of these, in whatever order we want:

class Foo():
    def whoami(self):
        print("I am Foo")

class Bar():
    def whoami(self):
        print("I am Bar")

class Baz(Bar, Foo):
    def whoami(self):
        Foo.whoami(self)
        Bar.whoami(self)

# Notice how Baz performs both actions starting from Foo, despite inheriting
# from Bar first
b = Baz()
b.whoami()

This trick may also be useful if parent's method names overlap, but require different arguments

class A():
    def whoami(self):
        print("I am A")

class B(A):
    pass

class C(A):
    def whoami(self, a):
        print("I am C")

class D(C, B):
    # If we will simply inherit things "as is", "whoami"'s resolution will stop
    # at C's method and attempting to call it without arguments will throw a
    # type error. To solve this, we specifically mention which method we'd want
    # to inherit from

    def whoami(self):
        A.whoami(self)

# "I am A"
d = D()
d.whoami()

Okay, cool. Seems like everything is pretty straightforward now. So lets look at a bit more complicated example:

class A():
    def whoami(self):
        print("I am A")

class B(A):
    pass

class C():
    def whoami(self):
        print("I am C")

class D(B, C):
    pass

d = D()
d.whoami()

So B is on left from C in D's inheritance order, thus D().whoami() returns "I am C". Nothing surprising there. But what if C also inherited from A?

class A():
    def whoami(self):
        print("I am A")

class B(A):
    pass

class C(A):
    def whoami(self):
        print("I am C")

class D(B, C):
    pass

# What will this print?
d = D()
d.whoami()

Suprisingly, this time we've got "I am C". But why? Well, both B and C are the descendants of A. Because of that, MRO looks for a matching method in both, and only then (if no matches have been found) goes for the ancestor. If C was to inherit from something else, we would still get the "I am A" response

class A():
    def whoami(self):
        print("I am A")

class X():
    def whoami(self):
        print("I am X")

class B(A):
    pass

class C(X):
    def whoami(self):
        print("I am C")

class D(B, C):
    pass

d = D()
d.whoami()

Multiple inheritance's capabilities are not limited to using one or another parent's methods. With power of MRO, we can also access one family member's methods from another. Look at this situation:

class A():
    def get_me(self):
        return "I am A"

    def whoami(self):
        print(get_me())

class B(A):
    pass

class C():
    def whoami(self):
        print("I am C, but I am also", self.get_me())

class D(C, A):
    pass

d = D()
d.whoami()

Here we have C accessing get_me() of A, because D inherits from both of these. This allows us to mix C's custom whoami() behavior with any other class that implements get_me(), without interfering with other functionality of that class. Attempting to use C without mixing it with class that provides the required functionality will still throw an AttributeError. Such classes that work as overrides/extensions to others are called "Mixins".

To sum things up: multiple inheritance allows us to use and mix quirks of multiple classes, which grant us ability to create more flexible and modular code. There are some things about MRO's behaviour you should keep in mind, but once they start "clicking", everything become logical and kinda self-explanatory. Thus said, you can still overengineer things and create an unmaintainable mess, so - like always - if you've got a powerfull tool, use it wisely!

@moonburnt
Copy link
Author

This is a blueprint / draft of an article. I've poured down my thoughts into text, but unsure if its structured/phrased good enough to read if you are newcomer who just want to get into topic without much unnecessary "water" or complicated matters.

Having said that, any feedback would be helpful

@Val325
Copy link

Val325 commented May 1, 2023

Very cool and easily explained

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment