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.
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.