This notebook contains an excerpt from the An Introduction To Python Programming And Numerical Methods For Scientists and Engineers; the content is available on GitHub.

The text is released under the CC-BY-NC-ND license, and code is released under the MIT license. If you find this content useful, please consider supporting the work by buying the book!

< 7.2 Class and Object | Contents | 7.4 Summary and Problems >

Inheritance

We have already seen the modeling power of the OOP using the class and object by combining data and methods. But there is one more important concept, inheritance, which make the OOP code more modular and easier to reuse. Inheritance can let a new class to process the data and methods from a defined class, and then we can add modifications on the class. Usually the new class is called a child class and the one that inherit from is called parent class or superclass. Back to the definition of the class structure, we can see a basic inheritance is class ClassName(superclass), which means the new class get all the attributes and methods from the superclass. Let’s try an example.

TRY IT! Define a class named Sensor with attributes name, location, and record_date that pass from the creation of an object and an attribute data as an empty dictionary to store data. It should one method add_data with t and data as arguments to take in timestamp and data arrays and put into the data attribute. In addition, it should have one clear_data method to delete the data.

class Sensor():
    def __init__(self, name, location, record_date):
        self.name = name
        self.location = location
        self.record_date = record_date
        self.data = {}
        
    def add_data(self, t, data):
        self.data['time'] = t
        self.data['data'] = data
        print(f'We have {len(data)} points saved')        
        
    def clear_data(self):
        self.data = {}
        print('Data cleared!')

Now we have a class to store general sensor information. We can create a sensor object to store some data.

EXAMPLE: Create a sensor object.

import numpy as np

sensor1 = Sensor('sensor1', 'Berkeley', '2019-01-01')
data = np.random.randint(-10, 10, 10)
sensor1.add_data(np.arange(10), data)
sensor1.data
We have 10 points saved
{'time': array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
 'data': array([ 3,  2,  5, -1,  2, -2,  6, -1,  5,  4])}

Inherit and extend new method

Now say we have two different type of sensors, one accelerometer and one temperature sensor, they do share the same attributes and methods as Sensor class, but they also have different attributes or methods need to be appended or modified from the original class, what should we do? Do we create two different classes from scratch? This is where the inheritance comes in to make life easier. These two new class will inherit from the Sensor class and get all the attributes and methods, we can decide whether we want to extend the attributes or methods. Let’s first create a new class Accelerometer and add a new method show_type to report what kind of sensor it is.

class Accelerometer(Sensor):
    
    def show_type(self):
        print('I am an accelerometer!')
        
acc = Accelerometer('acc1', 'Oakland', '2019-02-01')
acc.show_type()
data = np.random.randint(-10, 10, 10)
acc.add_data(np.arange(10), data)
acc.data
I am an accelerometer!
We have 10 points saved
{'time': array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
 'data': array([ -1,   4,   7, -10,  -2,  -6,   2,  -8,   9,   3])}

We can see that creating this new Accelerometer class is very simple, we inherit from Sensor (called superclass) and the new class actually contains all the attributes and methods from the superclass. We then add a new method show_type, which actually no existed in the Sensor class, we successfully extend the child class with a new methods. This shows the power of inheritance, you reused most part of the Sensor class in a new class, and extend the functionality. Also notice that, the modeling of the real-world entities are very logical in the OOP. The Sensor class as the parent class passes all the characteristics to the child class Accelerometer which are similar to our human society that we inherit a lot of our parents’ characteristics with our unique ones.

Inherit and update the method

Of course, we can replace some of the old methods with new methods just by simply redefine it.

EXAMPLE: Create a class TempSensor that inherit from Accelerometer but replace the show_type method that also print out the name of the sensor.

class TempSensor(Accelerometer):
    
    def show_type(self):
        print(f'I am {self.name}, measure temperature!')
        
temp = TempSensor('temp', 'Oakland', '2019-03-01')
temp.show_type()
I am temp, measure temperature!

We see that, our new TempSensor class actually override the method show_type with new features. We are not only inherit and extend the characteristics from our parent class, but also modified/improved some methods.

Inherit and update attributes with super

Let’s see we want to create a class NewSensor that inherit from Sensor class, but with updated the attributes by adding a new attribute brand. We can of course re-define the whole __init__ method as shown below.

class NewSensor(Sensor):
    def __init__(self, name, location, record_date, brand):
        self.name = name
        self.location = location
        self.record_date = record_date
        self.brand = brand
        self.data = {}
        
new_sensor = NewSensor('OK', 'SF', '2019-03-01', 'XYZ')
new_sensor.brand
'XYZ'

But we don’t want to re-type all the code as before for all the attributes, this is especially true if we have tens of attributes. Therefore, there is a more concise and popular way to do it: we can use the super method to avoid referring to the parent class explicitly. Let’s see the following example:

EXAMPLE: Use super method to redefine the attributes.

class NewSensor(Sensor):
    def __init__(self, name, location, record_date, brand):
        super().__init__(name, location, record_date)
        self.brand = brand
        
new_sensor = NewSensor('OK', 'SF', '2019-03-01', 'XYZ')
new_sensor.brand
'XYZ'

Now we can see with the super method, we avoid to list all the definition of the attributes, this helps keep your code maintainable for the foreseeable future. But it really useful when you are doing multiple inheritance, which is beyond the discussion of this book.

Multiple inheritance

Quickly we will show here that you can inherit from multiple classes instead of just a single one. For example, the above different classes have different features, the NewSensor class have a brand attribute, and the Accelerometer class has a new method show_type. If we want to have both of these features, we can inherit from both of them.

EXAMPLE: Create a new class BetterSensor that inherit directly from NewSensor class and Accelerometer class. Test that the new class do have the brand attribute and show_type method.

class BetterSensor(NewSensor, Accelerometer):
    pass

better_sensor = BetterSensor('OK', 'SF', '2019-03-01', 'XYZ')
better_sensor.show_type()
better_sensor.brand
I am an accelerometer!
'XYZ'

You can see that inherit from multiple classes is simple. But sometimes it may need caution if there is a conflict from the two superclasses, you need to think through which constructor and methods you are inheriting from. This is definitely adding in more complexity in your code, therefore, use with caution.

< 7.2 Class and Object | Contents | 7.4 Summary and Problems >