Intro to Inheritance with Python

by John | November 29, 2020

 

In this article we will discuss some key concepts relating to inheritance. We will begin with simple examples to illustrate concepts such as delegation and overriding. We will then finish with some links where you can real life example to show how these concepts can actually be used in real projects. Inheritance is a very important concept in object orientated programming and is widely used in real projects. This article should serve as a beginners introduction not an exhaustive list of all aspects of the topic.

 

When thinking about inheritance in the context of OOP, it may be useful to look at the definition of the word more generally and then contextualizing it to programming. The definition below was taken from  Oxfordlearnersdictionary.com:

 

1.  the money, property, etc. that you receive from somebody when they die; the fact of receiving something when somebody dies

2.  something from the past or from your family that affects the way you behave, look, etc.

 

 

What does this have to do with computer programming?

Well, the definition given above is very similar to the way parent to child relationship between classes we can create in code. Although you may be thankful to know; there is no death associated with the inheritance in programming! 

 

The way to create an inheritance chain in Python is shown below. If we want a class to inherit from another class. Below we create 3 child classes which inherit from the parent class. When Creating an inheritance chain we should consider what the future child classes will all have in common and define that information (attributes & methods) within the parent class. 

 

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

 

The child classes defined below are the DerivedClassName from above whereas the Parent class is the BaseClassName. Notice we can create as many derived classes (children) as we want. This is generally the main reason for using inheritance in the first place. 

 

class Parent:
    pass

class Child1(Parent):
    pass

class Child2(Parent):
    pass

class Child3(Parent):
    pass

 

Let's create another parent class and a child class below. We have created a property  and an instance method. Refer back to the generic definition of inheritance, and think about how these properties will be passed on to the child. 

 

class Parent:
    def __init__(self):
        self._name = 'I am the parent'
        
    
    @property
    def name(self):
        print('getter called for name ',
              f'from {self.__class__.__name__}')
        return self._name
        
    @name.setter
    def name(self, new_name):
        print('setter called for name ', 
              f'from {self.__class__.__name__}')
        self._name = new_name
        
    def intro(self):
        print(f'Hello I am being called from {self.__class__.__name__}',
              f'{self.name}')
        
  
class Child1(Parent):
    def __init__(self):
        self._name = 'I am a Child'


class Child2(Parent):
    def __init__(self):
        self._name = 'I am another Child'
        

class Child3(Parent):
    def __init__(self):
        self._name = 'One more Child'

 

Notice that in the Child1 class we have only defined the __init__ method. But we have inherited the property and instance method we defined in the Parent class. 

 

c1 =Child1()

c2 =Child2()

c3 = Child3()


c1.name,c2.name, c3.name
# ('I am a Child', 'I am another Child', 'One more Child')

c1.intro()
#Hello I am being called from Child1 I am a Child

c2.intro()
#Hello I am being called from Child2 I am another Child

c3.intro()
# Hello I am being called from Child3 One more Child

 

Notice above that the methods we are calling from the Child class are actually a part of the child class now. So anything we have defined in the parent class is now available to be called within the child class. 

 

Hopefully this shows why inheritance may be a useful thing to include in your code. In this case we saved ourselves approximately 10 x 3 = 30 lines of code as we simply inherited the methods that are common to all child classes. While it probably wouldn't be too much of a problem doing a bit of copy pasting in this contrived example, it is a much better practice to use inheritance. 

 

 

 

Polymorphism

Let's introduce an important concept that you will most likely hear quite often.

What does this mean? 

 

poly = means 'many' derived from Greek polus ‘much’, polloi ‘many’

 

For the morph part we have taken the meaning from biology as it is the most useful to make the point. 

morph = Local variety of a species, distinguishable from other populations of the species by morphology or behavior. 

 

Focus on the "distinguishable from other populations of the species by behavior" part of the definition above. This gives a good base to understand some important concepts in OOP. Below we will show some examples of polymorphism. 

 

 

Overriding 

Taking a new example class called Person which takes a name and age argument. The instance method called whoami is defined to print a message giving a description of the person's name. 

 

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        
    def whoami(self):
        print('Hello I am a Person called ', self._name)
    
    def how_old(self):
        print(f'I am {self.age} years old')
    

 

Below we create four child classes Person. Keep in mind the definition of polymorphism when thinking about how redefining the whoami and how_old methods. We override the whoami and how_old methods in two of the classes below. Notice they are named exactly the same way they were in the parent class. Recall the definition provided for polymorphism, we want the local versions to behave differently to others depending on the class, or perhaps we just want to keep the default behavior defined in the parent class. 

 

class Student(Person):
    def __init__(self, name, age):
        self._name = name 
        self._age = age
    
    def whoami(self):
        print('Hello I am a Student called ', self._name)
    
class SoftwareEngineer(Person):
    def __init__(self, name, age, language):
        self._name = name
        self._age = age
        self._language = language

    def whoami(self):
        print('Hello I am a software engineer called ', self._name)
        print('My language of choice is ', self._language)
    
class Waiter(Person):
    def __init__(self, name, age, address):
        self._name = name
        self._age = age
        self._address = address
    
    def how_old(self):
        print('Sorry that is not for you to know')
      
class Doctor(Person):
    def __init__(self, name, age, speciality):
        self._name = name
        self._age = age
        self._speciality = speciality 
    
    def how_old(self):
        print('It is not polite to ask a lady her age')

 

 

Let's create a few instances of the the classes about to see how they behave. 

 

Basically what Python what do in these cases is the following:

1) Check if the method has been defined in the child class, if so execute the method from the instance.  

2) If Not found in the child class Python will check in the parent class to see if it is defined there.

3) Raise an AttributeError if method/property not found in 1 or 2. 

 

person1 = Student('Peter', 20)
person1.whoami() # overriding the parent's method
# Hello I am a Student called  Peter
person1.how_old() # calling from parent
# I am 20 years old

person2 = SoftwareEngineer('Paul', 35, 'C')
person2.whoami() # overriding the parent's method
# Hello I am a software engineer called  Paul
# My language of choice is  C
person2.how_old() # calling from the parent's method
#I am 35 years old

person3 = Waiter('Jim', 26, '84 Codearmo street')
person3.whoami() # calling from the parent's method
# Hello I am a Person called  Jim
person3.how_old() # overriding the parent's method
# Sorry that is not for you to know


person4 = Doctor('Julia', 40, 'Surgery')
person4.whoami() # calling from the parent's method
# Hello I am a Person called  Julia
person4.how_old() # overriding the parent's method
#It is not polite to ask a lady her age

 

 

Delegation & super()

Notice above that we used the phrase "calling from the parent's method" in the script above, the technical term for this is called delegation so we are delegating the methods we call as opposed to overriding them in the methods above. As explained earlier in the article this can be quite useful.

 

What if we want to include delegation and overriding in our class? Well to accomplish this we need to introduce one of Python's built-in keywords called super(). This is a very useful method to include in our code. 

 

What is the super() method?

 

The super method is used to delegate a method to the parent class. The syntax for using this method is as follows. 

 

super().methodName(any_expected_args_here)

 

When we call this method Python will check the inheritance chain. And see if an attribute with the methodName shown above exists, if so use the parent's method instead of the child's. 

 

Let's take a simple example before implementing this on the Person / Occupation chain we created above. We will also shown that static methods are inherited exactly the same way as instance methods. We will use these static methods as validators to ensure the arguments passed are of the correct type. 

 

class Parent:
    def __init__(self, int_arg, str_arg):
        self.int_arg = Parent.is_int(int_arg)
        self.str_arg = Parent.is_str(str_arg)
        print('__init__ called from the parent class')
    
    @staticmethod
    def is_int(int_):
        if isinstance(int_, int):
            return int_
        else:
            raise TypeError('int_arg must be a integer, Parent says so')
            
    @staticmethod
    def is_str(str_):
        if isinstance(str_, str):
            return str_
        else:
            raise TypeError('str_arg must be string, Parent say so')



class Child(Parent):
    def __init__(self, int_arg, str_arg, child_arg):
        super().__init__(int_arg, str_arg)
        self.child_arg = child_arg
        print('__init__ called from child class')
        
        

c = Child(20, 'must be string', 'instance_arg')

#__init__ called from the parent class
#__init__ called from child class

 

So what has happened in the code above, is that we have delegated part of the instantiation in the constructor to the parent class. We have done this using the super().__init__(int_arg, child_arg).

Be careful not to include self in the super().__init__() this can be a nasty bug!! 

 

Let's use the super() method on the Person / Occupation code we created perviously. Notice that when we were creating the child classes for the child classes' constructor we were constantly setting self.name = name and self.age = age. Well we can use the super().__init__() method to just let the parent do this part. We also implement two static methods to check whether the variables passed into the constructor are valid. 

 

class Person:
    def __init__(self, name, age):
        self._name = Person.valid_name(name)
        self._age = Person.valid_age(age)
        print('__init__ called from parent')
        
    def whoami(self):
        print('Hello I am a Person called ', self._name)
    
    def how_old(self):
        print(f'I am {self._age} years old')
        
    @staticmethod
    def valid_name(name):
        if isinstance(name, str) and len(name) > 2:
            return name
        else:
            raise TypeError('Name must be as string longer than',
                            '2 character')
    
    @staticmethod
    def valid_age(age):
        if age > 0 and isinstance(age, int):
            return age
        else:
            raise TypeError('Age must be a positive integer')
    
        
class Student(Person):
    def __init__(self, name, age):
        super().__init__(name, age)
    
    def whoami(self):
        print('Hello I am a Student called ', self._name)
    
class SoftwareEngineer(Person):
    def __init__(self, name, age, language):
        super().__init__(name, age)
        self._language = language

    def whoami(self):
        print('Hello I am a software engineer called ', self._name)
        print('My language of choice is ', self._language)
    
class Waiter(Person):
    def __init__(self, name, age, address):
        super().__init__(name, age)
        self._address = address
    
    def how_old(self):
        print('Sorry that is not for you to know')
      
class Doctor(Person):
    def __init__(self, name, age, speciality):
        super().__init__(name, age)
        self._speciality = speciality 
    
    def how_old(self):
        print('It is not polite to ask a lady her age')
        
        
        
person1 = Student('Peter', 20)
person2 = SoftwareEngineer('Paul', 35, 'C')
person3 = Waiter('Jim', 26, '84 Codearmo street')
person4 = Doctor('Julia', 40, 'Surgery')

#__init__ called from parent
#__init__ called from parent
#__init__ called from parent
#__init__ called from parent

 

So that saved us quite a bit of code when we think about the validation we done on the attributes in the parent constructor. 

 

We can also use the super() method on instance attributes, let's try that with a few of the child classes. Recall that we override the how_old method for the Doctor class, by printing the phrase 'It is not polite to ask a lady her age'   what happens if we want to return the Person's how_old() method if the Doctor is a man, and the instance method if it is a woman. Well, we can make use of the super().how_old() method to achieve this. 

 

class SoftwareEngineer(Person):
    def __init__(self, name, age, language):
        super().__init__(name, age)
        self._language = language

    def whoami(self):
        print('Hello I am a software engineer called ', self._name)
        print('My language of choice is ', self._language)
        
        print('But I am also .....')
        #calling the parent method
        super().whoami()


class Doctor(Person):
    def __init__(self, name, age, speciality, gender):
        super().__init__(name, age)
        self._speciality = speciality 
        self.gender = gender
    
    def how_old(self):
        if self.gender == 'female':
            print('It is not polite to ask a lady her age') 
        else:
            super().how_old() 
            
        
person1 = SoftwareEngineer('Paul', 35, 'C')  
person1.whoami()
#Hello I am a software engineer called  Paul
#My language of choice is  C
#But I am also .....
#Hello I am a Person called  Paul

person2 = Doctor('Julia', 40, 'Surgery', 'female')
person2.how_old()
#It is not polite to ask a lady her age

person3 = Doctor('Tom', 50, 'Cardiology', 'male')
person3.how_old()
# I am 50 years old

 

Just to finish up we will show that the inheritance chain need not stop at just 1 parent 1 child, we can create more objects that inherit from the child, which still keeps the attributes defined up the inheritance chain. Let's take the Student class and add a HighSchoolStudent class which inherits from Student. 

 

class Person:
    def __init__(self, name, age):
        self._name = Person.valid_name(name)
        self._age = Person.valid_age(age)
        print('__init__ called from parent')
        
    def whoami(self):
        print('Hello I am a Person called ', self._name)
        return f'Hello I am a Person called {self._name}'
    
    def how_old(self):
        print(f'I am {self._age} years old')
        
    @staticmethod
    def valid_name(name):
        if isinstance(name, str) and len(name) > 2:
            return name
        else:
            raise TypeError('Name must be as string longer than',
                            '2 character')
    
    @staticmethod
    def valid_age(age):
        if age > 0 and isinstance(age, int):
            return age
        else:
            raise TypeError('Age must be a positive integer')
    
        
class Student(Person):
    def __init__(self, name, age):
        super().__init__(name, age)
    
    def whoami(self):
        return f'I am a student called {self._name}'
       
    

class HighSchoolStudent(Student):
    def __init__(self, name, age, school):
        super().__init__(name, age)
        self._school = school
    
    def whoami(self):
        s = super().whoami()
        s += f'\n I am a student at a school called {self._school}'
        print(s)
        

person1 = HighSchoolStudent('Peter', 15, 'ABC school')

person1.whoami()
#I am a student called Peter
#I am a student at a school called ABC school

 

Take a look at the inheritance chain and take note of the fact that we have the whoami method defined in each of the classes. When we called the super().whoami() from the HighSchoolStudent class Python returned the output from the Student class. 

 

How does Python decide which method to take? This is known as the method resolution order and we can take a look at it using the __mro__ special method. 

 

HighSchoolStudent.__mro__

# (__main__.HighSchoolStudent, __main__.Student, __main__.Person, object)

 

So this means Python will look in HighSchoolStudent first then Student and finally Person. Let's just take a quick example if we wanted the super().whoami() to call the method from the Person class as opposed to the Student. We can do this by going up the MRO using the super(Student, self).whoami() statement we define below. 

 

class HighSchoolStudent(Student):
    def __init__(self, name, age, school):
        super().__init__(name, age)
        self._school = school
    
    def whoami(self):
        s = super(Student, self).whoami()
        s += f'\n I am a student at a school called {self._school}'
        print(s)
        
        print('\n----------------------------------------\n')
        print('getting from student')
        s = super(HighSchoolStudent, self).whoami()
        s += f'\n I am a student at a school called {self._school}'
        print(s)
        
        
person1 = HighSchoolStudent('Peter', 15, 'ABC school')

person1.whoami()

#Hello I am a Person called Peter
# I am a student at a school called ABC school
#
#----------------------------------------
#
#getting from student
#I am a student called Peter
# I am a student at a school called ABC school

 

It's hard to think of a real use case for what we have done above with the super() method. But it doesn't hurt to know it!

 

Summary

- We can create an inheritance chain by creating a Parent object and then Child(Parent) classes for all classes we which to inherit from. 

- When thinking about inheritance we should consider what do a group of child classes have in common? Can we save ourselves some time by creating a parent class and inheriting aforementioned common attributes? 

- We can inherit property attributes, static methods, instance methods and class attributes using inheritance. 

- Polymorphism can be viewed as different behavior for attributes depending on where they are defined. Recall the local definition we gave for polymorphism. 

- We can override parent attributes by redefining them in the child class. 

- We can delegate to the parent using the super() keyword. 

- We can check the method order resolution using the __mro__ special method. 

 

 

Real examples

 

Algo trading backtest:

- Parent class is the BacktestSA which contains methods common to the individual strategy backtests. 

- Strategies which inherit from BacktestSA

1) Strategy 1

2) Strategy 2 

3 ) Strategy 3

4) Strategy 4

Notice that all these strategies inherit the methods they need from the BacktestSA parent class, this is useful because since there is quite a lot of code we don't want to type it all out each time we create a strategy.

 

Digital Signal Processing by Allan Downey

This is an interesting and very fun book I have been reading, You can find the code from this book on github here.

How does Allan use inheritance in his book? Which is available at greenteapress.com for free! 

Well, take the following extract of some of the classes he has created. He also makes use of overriding the evaluate method from the Signal class in each of the children classes. Although the code is a bit long, it could be a useful exercise to go over the script and try to match the concepts we covered in this article to the methods implemented in this script. 

 

class Signal: # view on lines 1211-1262 on github link above
    """Represents a time-varying signal."""

class Sinusoid(Signal): # view on lines 1314-1328 on github link above
    """Represents a sinusoidal signal."""

class ComplexSinusoid(Sinusoid):  # view on lines 1387-1400 on github link above
    """Represents a complex exponential signal."""

class SquareSignal(Sinusoid): # view on lines 1403-1417 on github link above
    """Represents a square signal."""

class SawtoothSignal(Sinusoid): # view on lines 1420-1434 on github link above
    """Represents a sawtooth signal."""

class ParabolicSignal(Sinusoid): # view on lines 1437-1452 on github link above
    """Represents a parabolic signal."""

class Chirp(Signal): # view on lines 1508-1561 on github link above
    """Represents a signal with variable frequency."""

class ExpoChirp(Chirp): # view on lines 1564-1580 on github link above
    """Represents a signal with varying frequency."""

 

 

 


Join the discussion

Share this post with your friends!