My Little Subclass: Inheritance is Magic

Sat 29 April 2017 by Moshe Zadka

Learning about Python Method Resolution Order with Twilight Sparkle and her friends.

(Thanks to Ashwini Oruganti for her helpful suggestions.)

The show "My Little Pony: Friendship is Magic" is the latest reimagination of the "My Little Pony" brand as a TV show. The show takes place, mostly, in Ponyville and features the struggles of the so-called "Mane Six" characters. Ponyville was founded by Earth ponies, but is populated by Unicorns and Pegasus ponies as well.

At the end of season 4, Twilight Sparkle becomes an "Alicorn" princess. Alicorns have both horns, like unicorns, and wings, like the pegasus ponies. In the My Little Pony show, horns are used to do magic -- most commonly, telekinetically moving objects.

When reaching puberty, ponies discover their "special talent" via a cutie mark -- a magical tattoo on their flank.

from __future__ import print_function
# Base class for all ponies.
class Pony(object):
    def __init__(self, name, cutie_mark_ability):
        self.name = name
        self.cutie_mark_ability = cutie_mark_ability

    def move(self):
        return "Galloping"

    def carry(self):
        return "Carrying on my back"

    def special_abilities(self):
        return [self.cutie_mark_ability]
# Earth ponies. Just regular ponies.
class Earth(Pony):
    pass
# Apple Pie is an Earth pony. Let's define her!
Apple_Pie = Earth("Apple Pie", "farming apples")
# This is a little helper function to help ponies
# introduce themselves.
def introduce(pony):
    print("Hi, I'm", pony.name, ",", type(pony).__name__, "pony")
    print("Moving:", pony.move())
    print("Carrying:", pony.carry())
    print("Abilities:")
    for ability in pony.special_abilities():
        print("\t", ability)
introduce(Apple_Pie)
Hi, I'm Apple Pie , Earth pony
Moving: Galloping
Carrying: Carrying on my back
Abilities:
     farming apples
# Pegasus ponies have wings and can fly.
class Pegasus(Pony):

    def move(self):
        return "Flying"

    def special_abilities(self):
        return super(Pegasus, self).special_abilities() + ["flying"]
# Rainbow Dash is a pegasus. Let's define her!
Rainbow_Dash = Pegasus("Rainbow Dash", "rainbow boom")

introduce(Rainbow_Dash)
Hi, I'm Rainbow Dash , Pegasus pony
Moving: Flying
Carrying: Carrying on my back
Abilities:
     rainbow boom
     flying
# Unicorn ponies have wings and can fly.
class Unicorn(Pony):

    def carry(self):
        return "Lifting with horn"

    def special_abilities(self):
        return super(Unicorn, self).special_abilities() + ["magic"]
Rarity = Unicorn("Rarity", "finding gems")

introduce(Rarity)
Hi, I'm Rarity , Unicorn pony
Moving: Galloping
Carrying: Lifting with horn
Abilities:
     finding gems
     magic
# Alicorn princesses have wings and horns
class Alicorn(Pegasus, Unicorn):

    def special_abilities(self):
        return super(Alicorn, self).special_abilities() + ["ruling"]

# Twilight Sparkle is an alicorn princess.
Twilight_Sparkle = Alicorn("Twilight Sparkle", "learning magic")

introduce(Twilight_Sparkle)
Hi, I'm Twilight Sparkle , Alicorn pony
Moving: Flying
Carrying: Lifting with horn
Abilities:
     learning magic
     magic
     flying
     ruling

Pun fully intended -- this is magic! To understand why, let us think of how the inheritance hierarchy of the Alicorn class looks like:

    Pony
    /   \
   /     \
Unicorn Pegasus
   \     /
    \   /
    Alicorn

The Unicorn class has a move method it inherits from Pony. But magically, the Alicorn class knows it should get the more specialized one from Pegasus. Could it be that when inheriting, Python looks right to left? But then the carry method would not work correctly. Lastly, what about special_abilities? How did it track all classes correctly?

The answer is the MRO -- message resolution order.

Alicorn.mro()
[__main__.Alicorn, __main__.Pegasus, __main__.Unicorn, __main__.Pony, object]

Let us first explain the last question. The meaning of super(klass, instance).method() is "find the next time the method appears in the instance's class, after klass does". Because it looks at the MRO of the instance's class, it knows to jump from Alicorn to Pegasus to Unicorn and finally to Pony.

The MRO also helps to explain the first question, but the answer is more complex. Let us focus on the carry method.

Pegasus.carry == Pony.carry
True

This is the first clue -- the methods compare equal. Which brings us to our next point: let's list all candidate methods.

candidates = set()
for klass in Alicorn.mro()[1:]:
    try:
        candidates.add(klass.carry)
    except AttributeError:
        pass
candidates
{<unbound method Unicorn.carry>, <unbound method Pegasus.carry>}

(We do not see Pony.carry in the set because the Pegasus.carry appears first, Pony.carry compares equal, and candidates is a set.)

We only care about the last time a method appears in the MRO: if you think of it as "distance", we are looking for the closest method. Since in Python, it is easier to find the first time an element appears in a list, we will calculate the reversed MRO:

ordered_candidates = [getattr(klass, 'carry', None) for klass in Alicorn.mro()[1:]]
reverse_ordered_candidates = ordered_candidates[::-1]
reverse_ordered_candidates
[None,
 <unbound method Pony.carry>,
 <unbound method Unicorn.carry>,
 <unbound method Pegasus.carry>]

Now we find the candidate with the highest reverse-distance (lowest distance) using Python's max:

max(candidates, key=reverse_ordered_candidates.index)
<unbound method Unicorn.carry>

The same logic would explain the move method, and the details of the special_abilities method.

The Python Method Resolution Order (MRO) is subtle, and it is easy to misunderstand. Hopefully, the examples here, and the sample code, give a way to think about the MRO and understand why, exactly, it is the way it is.