MicroPython : Attributes and Properties

Contents

Introduction

MicroPython is a “slim” version of Python specifically designed with a small footprint to efficiently run on memory constrained microcontrollers.

MicroPython doesn't implement anywhere near the complete complement of Python functionality. However the subset that is implemented is useful, fit-for-purpose and mostly all that is required.

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

Attributes

Put simply, attributes are variables that are defined in classes. There are two types of attributes:

  • Class attributes
  • Instance attributes

Class attributes are defined in the class body, not in the methods. If the value of a class attribute is changed, the new value will be seen by all object instances of that class.

Instance attributes are defined using the self keyword, usually in the __init__() constructor method. The values stored in instance attributes are unique to each instance created from the class. Changing the value of an instance attribute will only change the value in that specific object instance.

An example is the easiest way to visualise the difference between class and instances attributes.

Example 1

# Demonstrate class and
# instance attributes.

class speedometer:
    # Class attribute
    class_speed = 100
    
    # Constructor
    def __init__(self, speed = 50):
        # Instance attribute
        self.speed = speed

mySpeedo = speedometer(60)
# Default speed = 50
herSpeedo = speedometer()

print("Class attribute 'class_speed'...")
print('mySpeedo class_speed:',
       mySpeedo.class_speed)
print('herSpeedo class_speed:',
       herSpeedo.class_speed)

# Change the value of class_speed
# attribute. This make the same
# change for both instances:
# mySpeedo and herSpeedo.
speedometer.class_speed = 120
print("Altering'class_speed' to 120")
print('mySpeedo class_speed:',
       mySpeedo.class_speed)
print('herSpeedo class_speed:',
       herSpeedo.class_speed)

print("\nInstance attribute 'speed'...")
print('mySpeedo speed:',
       mySpeedo.speed)
print('herSpeedo speed:',
       herSpeedo.speed)

# Change the value of the speed
# attribute of the instance
# mySpeedo. This will not change
# the value of the speed attribute
# for the instance herSpeedo.
print('Altering mySpeedo.speed to 200')
mySpeedo.speed = 200
print('mySpeedo.speed:',
       mySpeedo.speed)
print('herSpeedo.speed:',
       herSpeedo.speed)
          
Output:

Class attribute 'class_speed'...
mySpeedo class_speed: 100
herSpeedo class_speed: 100
Altering'class_speed' to 120
mySpeedo class_speed: 120
herSpeedo class_speed: 120

Instance attribute 'speed'...
mySpeedo speed: 60
herSpeedo speed: 50
Altering mySpeedo.speed to 200
mySpeedo.speed: 200
herSpeedo.speed: 50
          

The value of class attribute class_speed can be changed by a direct call on the class. This new value will be seen from all instances of the class; in this example: mySpeedo and herSpeedo.

On the other hand, if the instance attribute speed is changed in one of the instances eg mySpeedo, the speed attribute value of the other instance herSpeedo will not have changed.

Properties

In the true spirit of OOP encapsulation, attributes should be implemented as private variables. This means that any program using the class should not be able to make direct calls to the variable as is happening in Example 1 above.

MicroPython (and Python) does not have the keyword private[1] that is ubiquitous in other languages such as C++.

Furthermore, the class should provide public methods that at a minimum allow the setting and retrieval of the parameter's value. When this is implemented in the class, these methods become known as properties.

MicroPython (and Python) provide two mechanisms for the establishment of a property to provide controlled access to an attribute. Firstly, there is the property() function and secondly, the @property decorator[2].

MicroPython has several built-in decorators that are part of the language. The @property , @classmethod , and @staticmethod decorators are used to annotate methods of a class. The final section of this article will examine the @property decorator.

The property() Function

Description

Managed attributes can be created in classes with MicroPython's property() function.

Managed attributes, also known as properties, are useful when needing to modify the internal implementation of a class without changing its public interface. This allows improved/additional functionality to be introduced into a class without breaking any existing usage of this class.

Syntax

property(fget=None, fset=None, fdel=None, doc=None)

Parameters:
  fget: Function to return the value of
        the managed attribute.

  fset: Function to set the value of
        the managed attribute

  fdel: Function that defines how to
        handle the deletion of the
        managed attribute.

  doc: Docstring available when using
        the help() function.

            
Example 2

# Demonstrates usage of property() function.
# The property() function allows getter()
# and setter() methods to be defined for
# controlled access to an attribute.

# A class to implement a cartesian point.
class point:
    # Constructor with a default
    # point defined at (0,0)
    def __init__(self, x=0, y=0):
        self.x = point.verified(x)
        self.y = point.verified(y)

    # Checks that value is a number
    def verified(value):
        if isinstance(value, (int, float)):
            return value
        else:
            return None

    # Setter method to set x value
    def setx(self, value):
        self._x = point.verified(value)

    # Getter method to get x value
    def getx(self):
        return self._x

    # Setter method to set y value
    def sety(self, value):
        self._y = point.verified(value)

    # Getter method to get y value
    def gety(self):
        return self._y

    # Create x property
    x = property(getx, setx)
    # create y property
    y = property(gety, sety)

# Creating a new point object
# with default co-ordinates (0,0).
P1 = point()
# Getting the co-ordinates of the point.
print('Co-ordinates of P1:', P1.x, ',', P1.y)

# Setting new co-ordinates P1.
P1.x = 4
P1.y = 7
# Getting the new co-ordinates.
print('New co-ordinates of P1:', P1.x, ',', P1.y)

# Create a new point and test the
# number verification method by
# attempting to set y = 'Spam'.
P2 = point(6.2, 'Spam')
# Getting the co-ordinates of P2.
print()
print('Co-ordinates of point P2:', P2.x, ',', P2.y)

            
Output:

Co-ordinates of P1: 0 , 0
New co-ordinates of P1: 4 , 7

Co-ordinates of point P2: 6.2 , None

            

In this example the property() function is used to define properties x and y. Each of these properties now have defined getter() methods (getx() and gety()) and setter() methods (setx() and sety()).

The properties can now be called to get and set the co-ordinates of a point without the main program needing to directly access the underlying 'private' attributes _x and _y.

The @property Decorator

While the property() function as described and demonstrated above works perfectly ok, the use of the @property decorator is now the recommended way to create properties. The Example 2 has been rewritten with the @property decorator replacing the property() function.

Example 3

# Demonstrates the @property decorator.
# This is now the recommended way to
# create properties for controlled access
# to attributes.

# A class to implement a cartesian point.
class point:
    # Constructor with a default
    # point defined at (0,0)
    def __init__(self, x=0, y=0):

        self.x = point.verified(x)
        self.y = point.verified(y)

    # Checks that value is a number.
    def verified(value):
        if isinstance(value, (int, float)):
            return value
        else:
            return None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = point.verified(value)

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = point.verified(value)

# Creating a new point object
# with default co-ordinates (0,0).
P1 = point()
# Getting the co-ordinates of the point.
print('Co-ordinates of P1:', P1.x, ',', P1.y)

# Setting new co-ordinates P1.
P1.x = 4
P1.y = 7
# Getting the new co-ordinates.
print('New co-ordinates of P1:', P1.x, ',', P1.y)

# Create a new point and test the
# number verification method by
# attempting to set y = 'Spam'.
P2 = point(6.2, 'Spam')
# Getting the co-ordinates of P2.
print()
print('Co-ordinates of point P2:', P2.x, ',', P2.y)
            
Output:

Co-ordinates of P1: 0 , 0
New co-ordinates of P1: 4 , 7

Co-ordinates of point P2: 6.2 , None
            

Before the property() function can be called, methods must be defined to get and set respectively the attribute value. The property() function then ties these methods together to create the property.

The @property decorator very much simplifies this process. The getter and setter are directly defined with the creation of the property.