MicroPython Class Inheritance

Contents

Introduction

MicroPython is a “slim” version of Python specifically designed with a small footprint to efficiently run on memory constrained microcontrollers. MicroPython implements most of the Python class inheritance functionality.

It is assumed that the reader has some familiarity with object-oriented programming concepts.

All examples in this article are original and have been tested on a BBC micro:bit for correctness using the Mu Editor.

Definition

There are many excellent online references to Python class inheritance. Consider the following introduction to the topic from programiz.com:

Like any other OOP languages, Python also supports the concept of class inheritance.

Inheritance allows us to create a new class from an existing class.

The new class that is created is known as subclass (child or derived class) and the existing class from which the child class is derived is known as superclass (parent or base class).

Syntax


# define a parent
class parent_class:
    # attributes and method definition

# inheritance
class child_class(parent_class):
    # (inherited) attributes and methods
    # of the parent_class
    #        PLUS 
    # attribute and method definitions
    # of the child_class.

          

A child class is designated by referencing the parent class in the class definition header. The child class now inherits all attributes and methods of the parent.

Class Inheritance in Action

The following code example implements a class Sensor() without inheritance.

Example 1

# Example of a class with no inheritance.

# Simulation that implements the operation
# of sensors.
# Defines a single class for a sensor.
# Two actual sensors are implemented
# as methods of the Sensor class.

# Used to generate random numbers.
from random import randint

# A class that reads and returns values
# for two different sensors.
# (1) S180 is a high precision sensor.
# (2) S90 is a low precision sensor.
class Sensor:
    # Constructor
    def __init__(self,
                 model='generic',
                 mean=25,
                 variance=0.85):
        self.model = model
        # Parameters used to simulate sensor read
        self.mean = mean
        self.variance = variance

    def SerialNum(self):
       return hash(self)

    def read(self):
        if self.model == 'S180':
            # High precision sensor.
            # Simulated S180 sensor read.
            # Returns a randomly generated
            # reading from a 'tight' range.
            lower = int((self.mean - self.variance) * 10)
            upper = int((self.mean + self.variance) *10)
            value = randint(lower, upper)
            return round(value/10, 1)
        elif self.model == 'S90':
            # Low precision sensor.
            # Simulated S90 sensor read.
            # Returns a randomly generated
            # reading from a 'loose' range.
            lower = int((self.mean - self.variance) * randint(95, 100)/10)
            upper = int((self.mean + self.variance) * randint(95, 100)/10)
            value = randint(lower, upper)
            return round(value/10, 1)
        else:
            # Any other model sensor
            pass
            return 'Sensor has been read'

# Create an S180 high precision sensor.
myS180 = Sensor('S180', 24.5, 0.15)
print('S180 serial number is', myS180.SerialNum())
print('Reading from S180:', myS180.read())

# Create the default generic sensor.
mySensor = Sensor(mean=21, variance=0.45)
print('\nSensor serial number is', mySensor.SerialNum())
print('Reading from my Sensor:', mySensor.read())

# Create an S90 low precision sensor
myS90 = Sensor(model='S90', mean=21)
print('\nS90 serial number is', myS90.SerialNum())
print('Reading from S90:', myS90.read())

Output:

S180 serial number is 536891024
Reading from S180: 24.3

Sensor serial number is 536891488
Reading from my Sensor: Sensor has been read

S90 serial number is 536891216
Reading from S90: 19.8

The above code was run on a micro:bit. Since no sensor was actually connected to the microcontroller, the read() method includes an algorithm to simulate a sensor read. Mean and variance values are used to randomly generate a read value.

There is a development and maintenance issue with the Sensor() class as defined. The class code needs to be modified each time a new model sensor is used.

The class constructor is passed the model name of the sensor each time a Sensor object is instantiated. When the sensor object performs a read the model name is passed as an argument to the read() method where it is used in an if-elif-else conditional statement. This conditional statement must be extended each time a new model of sensor is added to the class.

A must better solution would be to define a base (parent) Sensor() class then define a child class for each model sensor that then implements the specific read() method for that sensor type.

Example 2 demonstrates this idea.

Example 2

# Example that uses class inheritance.

# Simulation that implements the operation
# of sensors.

# Defines a class for a virtual sensor.
# Two actual sensors are defined as subclasses
# of the virtual sensor.

# Used to generate random numbers.
from random import randint

# Base class that defines a virtual sensor.
class Sensor:
    # Constructor
    def __init__(self,
                 mean = 25,
                 variance = 0.85):
        # Parameters used to simulate sensor read.
        self.mean = mean
        self.variance = variance

    def SerialNum(self):
       return hash(self)

    def read(self):
        return 'Sensor has been read'

# Derived class that inherits from the
# Sensor base class.
# It extends the base class with a
# method to specifically read a S180 sensor.
class S180(Sensor):
   # High precision sensor

    def read(self):
        # Simulated S180 sensor read
        lower = int((self.mean - self.variance) * 10)
        upper = int((self.mean + self.variance) *10)
        value = randint(lower, upper)
        return round(value/10, 1)

# Derived class that inherits from the
# Sensor base class.
# It extends the base class with a
# method to specifically read a S90 sensor.
class S90(Sensor):
    # Low precision sensor

    def read(self):
        # Simulated S90 sensor read.
        lower = int((self.mean - self.variance) * randint(95, 100)/10)
        upper = int((self.mean + self.variance) * randint(95, 100)/10)
        value = randint(lower, upper)
        return round(value/10, 1)

# Create an S180 high precision sensor.
myS180 = S180()
print('S180 serial number is', myS180.SerialNum())
print('Reading from S180:', myS180.read())

# Create a generic sensor.
# This will use the generic read() method
# from the Sensor base class.
mySensor = Sensor(21, 0.45)
print('\nSensor serial number is', mySensor.SerialNum())
print('Reading from generic Sensor:', mySensor.read())

# Create an S90 low precision sensor.
myS90 = S90(mean=21)
print('\nS90 serial number is', myS90.SerialNum())
print('Reading from S90:', myS90.read())

Output:

S180 serial number is 536891184
Reading from S180: 25.6

Sensor serial number is 536891808
Reading from generic Sensor: Sensor has been read

S90 serial number is 536891648
Reading from S90: 19.5

Each new type of sensor is given its own class which inherits from the parent Sensor() class. The parent class provides the constructor --init--() and the SerialNum() method as this functionality is common for all sensor types. Each child class implements its own specific read() method which overrides the parent class's method.

This ability to define the same method for different classes of objects is known as polymorphism. At runtime the MicroPython interpreter sorts out which method to use based on the object's type.

Example 2 has only a single parent class. It is possible to extend this with the parent class being a child of another superclass. Additionally, a child class may inherit from more than one parent class.

Example 3 illustrates this with a three level hierarchy of inheritance and two parents at the top level and the middle level.

Python Class Inheritance
FIG 2 - Example 3 Class Inheritance
Example 3

# Demonstrates classes with multiple
# levels of inheritance.

class GrandParent1:
    def __init__(self, name):
        self.FirstName = name
        
    def PrintFirstName(self):
        print('GrandParent1 says first name is', self.FirstName)
    
class GrandParent2:
    def PutAge(self, age):
        self.age = age
    
    def PrintAge(self):
        print('GrandParent2 says age is', self.age)
    
class Parent1(GrandParent1):
    def PutSurname(self, name):
        self.Surname = name
        
    def GetFullName(self):
        return self.FirstName + ' ' + self.Surname
        
    def PrintFullName(self):
        print('Parent 1 says full name is', self.GetFullName())
    
class Parent2(GrandParent2):
    def PrintAge(self):
        print('Parent2 says age next birthday is', self.age+1)
    
class Child1(Parent1, Parent2):
    def PrintDetails(self):
        details = self.GetFullName() + ': age is ' + str(self.age)
        print('Child1 says', details)
    
mychild = Child1('James')

mychild.PutAge(21)
mychild.PutSurname('Smith')

mychild.PrintFirstName()
mychild.PrintFullName()
mychild.PrintAge()
mychild.PrintDetails()

Output:

GrandParent1 says first name is James
Parent 1 says full name is James Smith
Parent2 says age next birthday is 22
Child1 says James Smith: age is 21

This is a deliberately complicated (and useless!) example but has been developed to demonstrate the flexibility of MicroPython's class inheritance capabilities.

Put very simply; when the MicroPython interpreter requires access to a property or method it starts at the bottom of the inheritance lineage - in this case Child1 and moves upwards till the first instance of the resource is found.

If the property or method also exists at a higher inheritance level than that one is considered to be overridden by the lower occurrence. For example; GrandParent2 and Parent2 both have a method PrintAge(). If the object has been created from the class Child1 then the Parent2 version will be used.