{ Inheritance and MRO. }

Objectives

By the end of this chapter, you should be able to:

  • Explain what inheritance is and how it is used to reduce code duplication
  • Understand what the super keyword does
  • Define MRO and explain how it is used in Python 3

Inheritance and super

In many languages that support object-oriented programming, you can define a class which inherits from another class. Python takes things even further; in Python, we can do what is called multiple inheritance. This means that one class can inherit from many other classes!

Here's an example of single inheritance:

class Vehicle:
    def __init__(self,make,model,year):
        self.make = make
        self.model = model
        self.year = year
    def honk(self):
        return "Beep!"

class Car(Vehicle):
    def __init__(self,make,model,year):
        super().__init__(make,model,year) # this is python3 specific!
        self.wheels = 4

Notice that when we define the Car class, we pass in the Vehicle class. Then, inside of the car's __init__ method we make a call to super().__init__, which refers to the parent class's __init__ method. Calling super in this way ensures that car instances are assigned a make, model, and year, but saves us from having to repeat ourselves in the logic for the Car's __init__ method.

Inheritance also saves us from having to duplicate instance methods: instances of the child class automatically gain access to instance methods in the parent class. Take a look:

mack_truck = Vehicle("Mack","Titan",2015)
car = Car("Honda","Civic",2004)

mack_truck.honk() # "Beep!"
car.honk() # "Beep!"

In this case, we don't need to define a honk method for cars. Since the Car class inherits from Vehicle, any car instances will automatically have access to the honk method from the Vehicle class.

Multiple Inheritance and MRO

The car and vehicle example is fine when we want a class to inherit from one other class. But what if we want it to inherit from multiple classes? In this case, Python lets us do multiple inheritance by passing in multiple classes when we create a new class! Here's an example:

class Aquatic:
  def __init__(self,name):
    self.name = name

  def swim(self):
    return "{} is swimming".format(self.name)

  def greet(self):
    return "I am {} of the sea!".format(self.name)

class Ambulatory:
  def __init__(self,name):
    self.name = name

  def walk(self):
    return "{} is walking".format(self.name)

  def greet(self):
    return "I am {} of the land!".format(self.name)

class Penguin(Aquatic, Ambulatory):
  def __init__(self,name):
    super().__init__(name=name) 

jaws = Aquatic("Jaws")
lassie = Ambulatory("Lassie")
captain_cook = Penguin("Captain Cook")

Here we define two classes: Aquatic (for things that can swim) and Ambulatory (for things that can walk). Instances of the Aquatic class have a name, a swim method, and a greet method. Similarly, instances of the Ambulatory class have a name, a walk method, and a greet method.

We then define a third class called Penguin, which inherits from both Aquatic and Ambulatory. Finally, we create an instance from each of these classes.

Let's examine what happens when we try to call methods on each of these instances. You should see the following:

jaws.swim() # 'Jaws is swimming'
jaws.walk() # AttributeError: 'Aquatic' object has no attribute 'walk'
jaws.greet() # 'I am Jaws of the sea!'

lassie.swim() # AttributeError: 'Ambulatory' object has no attribute 'swim'
lassie.walk() # 'Lassie is walking'
lassie.greet() # 'I am Lassie of the land!'

captain_cook.swim() # 'Captain Cook is swimming'
captain_cook.walk() # 'Captain Cook is walking'
captain_cook.greet() # 'I am Captain Cook of the sea!'

Notice that because Penguin inherits from both Aquatic and Ambulatory, instances of the Penguin class have access to both the swim method and the walk method.

What happens with the greet method, though? In this case, there's a conflict, since both Aquatic and Ambulatory have their own greet methods! In this case, it looks like Penguin inherits the greet method from Aquatic and ignores the greet method from Ambulatory. (Notice that when we defined Penguin, we passed in Aquatic first, and then Ambulatory; what happens if you switch the order?)

So how does Python know where to search for methods? Whenever you create a class, Python sets a Method Resolution Order, or MRO, for that class. This order determines the order in which Python will look for methods on instances of that class. With single inheritance (or even simple examples of multiple inheritance) the order isn't so hard to deduce, but for really complex multiple inheritance things can get tricky. If you ever need to see what the MRO is for a class, you can take a look at the __mro__ attribute for that class, use the mro() method, or, even better, get some help:

Penguin.__mro__ # (<class 'multiple.Penguin'>, <class 'multiple.Aquatic'>, <class 'multiple.Ambulatory'>, <class 'object'>)

# OR

Penguin.mro() # returns a list to us

# EVEN BETTER!

help(Penguin) # gives us a detailed chain 

When you're ready, move on to Special Methods and Polymorphism

Continue