Object Oriented Programming: inheritance

Last week we learned the basic structure of python classes and introduced new semantics: classes, instances, methods, attributes... Today we have a short unit about a very important feature of most OOP languages: inheritance.

Class inheritance: introduction

Inheritance is a core concept of OOP. It allows a subclass (also called "child class") to override or extend methods and attributes from a base class (also called "parent class"). In other words, child classes inherit all of the parent's attributes and behaviors but can also specify new behaviors or replace old ones.

This is best shown with an example: let's make the Cat and Dog class inherit from the Pet class.

In [1]:
class Pet:

    # Initializer
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight
        
    def eat_food(self, food):
        self.weight += food
        
    @property
    def weight_lbs(self):
        return self.weight / 0.45359237
    
    def say_name_loudly(self):
        return self.say_name().upper()

class Cat(Pet):
    
    # Class attribute
    language = 'Meow'
        
    # Method
    def say_name(self):
        return '{}, my name is {} and I am nice!'.format(self.language, self.name)
        
class Dog(Pet):
    
    # Class attribute
    language = 'Woof'
        
    # Method
    def say_name(self):
        return '{}, my name is {} and I smell bad!'.format(self.language, self.name)

Let's advance through this example step by step.

First, let's have a look at the Pet class. It is a standard class defined the exact same way as in the previous lecture. Therefore, it can be instantiated and will work as expected:

In [2]:
p = Pet('PetName', 10)
p.weight_lbs
Out[2]:
22.046226218487757

The functionality of the class Pet however is very general, and it is unlikely to be used alone (a pet isn't specific enough to most people: is it a cat, a fish, a dog?). We used this class to implement the general functionality supported by all pets: they have a name and a weight, regardless of their species.

Now comes the important part: the Cat and Dog classes make use of these functionalities by inheriting from the Pet parent class. This inheritance is formalized in the class definition class Cat(Pet). The code of the two child classes is remarkably simple: it adds only a new functionality to the ones already inherited from Pet. For example:

In [3]:
c = Cat('Kitty', 4)
c.say_name()
Out[3]:
'Meow, my name is Kitty and I am nice!'

The Pet instance methods are still available:

In [4]:
c.eat_food(0.2)
c.weight
Out[4]:
4.2
In [5]:
d = Dog('Charlie', 8)
d.say_name()
Out[5]:
'Woof, my name is Charlie and I smell bad!'

There is a pretty straightforward rule for the behavior of child classes instances: when the called method or attribute is available at the child class level, it will be used (even if also available at the parent class level: this is called overriding, and will be explained next week); if not, use the parent class implementation.

This is exactly what happens in the code above: eat_food and weight are defined in the Pet class but are available for both Cat and Dog instances. say_name, however, is a child class instance method and can't be used by Pet instances.

This relationship between parent and child classes can be formalized as following:

In [6]:
print('Is d a Dog?', isinstance(d, Dog))
print('Is d also a Pet?', isinstance(d, Pet))
print('Is d also a Cat?', isinstance(d, Cat))
Is d a Dog? True
Is d also a Pet? True
Is d also a Cat? False

However, a Pet is neither a Cat or a Dog:

In [7]:
print('Is p a Dog?', isinstance(p, Dog))
print('Or a Cat?', isinstance(p, Cat))
Is p a Dog? False
Or a Cat? False

So, what about the say_name_loudly method? Although available for Pet instances, calling it will raise an error:

In [8]:
p = Pet('PetName', 10)
p.say_name_loudly()  # this raises an AttributeError
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-77f67e02018c> in <module>()
      1 p = Pet('PetName', 10)
----> 2 p.say_name_loudly()  # this raises an AttributeError

<ipython-input-1-7f872ef6cff6> in say_name_loudly(self)
     14 
     15     def say_name_loudly(self):
---> 16         return self.say_name().upper()
     17 
     18 class Cat(Pet):

AttributeError: 'Pet' object has no attribute 'say_name'

What happened here? It's correct, the class "Pet" has no say_name method!

In fact, it was intended behavior: since say_name_loudly is available to the child class instances, the method will work for them! See for instance:

In [9]:
d = Dog('Charlie', 8)
d.say_name_loudly()
Out[9]:
'WOOF, MY NAME IS CHARLIE AND I SMELL BAD!'

This is a typical use case for class inheritance in OOP: it allows code re-use. We will talk about more advanced use cases next week.

Take home points

  • class inheritance allows to share code and functionality between classes which are similar, but not the same
  • methods and attributes available at the parent class level are also available at the child class level, but not the other way around

This was only a (very) brief introduction to the concept of inheritance. Next week we will discuss concrete (and more advanced) use cases for inheritance and OOP in general.

What's next?