Working with classes¶
Of course, python would not be python without classes. A module can also include the implementation of classes. The procedure is similar to what we have already seen in the context of standard functions, except that we have to define a structure that holds at least a string with the name of the class, a pointer to the initialisation and printout functions, and a local dictionary. A typical class structure would look like
STATIC const mp_rom_map_elem_t simpleclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_method1), MP_ROM_PTR(&simpleclass_method1_obj) },
{ MP_ROM_QSTR(MP_QSTR_method2), MP_ROM_PTR(&simpleclass_method2_obj) },
...
}
const mp_obj_type_t simpleclass_type = {
{ &mp_type_type },
.name = MP_QSTR_simpleclass,
.print = simpleclass_print,
.make_new = simpleclass_make_new,
.locals_dict = (mp_obj_dict_t*)&simpleclass_locals_dict,
};
The locals dictionary, .locals_dict
, contains all user-facing
methods and constants of the class, while the simpleclass_type
structure’s name
member is what our class is going to be called.
.print
is roughly the equivalent of __str__
, and .make_new
is the C name for __init__
.
In order to see how this all works, we are going to implement a very simple class, which holds two integer variables, and has a method that returns the sum of these two variables. In python, a possible realisation could look like this:
class myclass:
def __init__(self, a, b):
self.a = a
self.b = b
def mysum(self):
return self.a + self.b
A = myclass(1, 2)
A.mysum()
3
In addition to the class implementation above and in order to show how class methods and regular functions can live in the same module, we will also have a function, which is not bound to the class itself, and which adds the two components in the class, i.e., that is similar to
def add(class_instance):
return class_instance.a + class_instance.b
add(A)
3
(Note that retrieving values from the class in this way is not exactly elegant, nor is it pythonic. We would usually implement a getter method for that.)
https://github.com/v923z/micropython-usermod/tree/master/snippets/simpleclass/simpleclass.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
typedef struct _simpleclass_myclass_obj_t {
mp_obj_base_t base;
int16_t a;
int16_t b;
} simpleclass_myclass_obj_t;
const mp_obj_type_t simpleclass_myclass_type;
STATIC void myclass_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
simpleclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "myclass(");
mp_obj_print_helper(print, mp_obj_new_int(self->a), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_int(self->b), PRINT_REPR);
mp_print_str(print, ")");
}
STATIC mp_obj_t myclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 2, 2, true);
simpleclass_myclass_obj_t *self = m_new_obj(simpleclass_myclass_obj_t);
self->base.type = &simpleclass_myclass_type;
self->a = mp_obj_get_int(args[0]);
self->b = mp_obj_get_int(args[1]);
return MP_OBJ_FROM_PTR(self);
}
// Class methods
STATIC mp_obj_t myclass_sum(mp_obj_t self_in) {
simpleclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_int(self->a + self->b);
}
MP_DEFINE_CONST_FUN_OBJ_1(myclass_sum_obj, myclass_sum);
STATIC const mp_rom_map_elem_t myclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_mysum), MP_ROM_PTR(&myclass_sum_obj) },
};
STATIC MP_DEFINE_CONST_DICT(myclass_locals_dict, myclass_locals_dict_table);
const mp_obj_type_t simpleclass_myclass_type = {
{ &mp_type_type },
.name = MP_QSTR_simpleclass,
.print = myclass_print,
.make_new = myclass_make_new,
.locals_dict = (mp_obj_dict_t*)&myclass_locals_dict,
};
// Module functions
STATIC mp_obj_t simpleclass_add(const mp_obj_t o_in) {
simpleclass_myclass_obj_t *class_instance = MP_OBJ_TO_PTR(o_in);
return mp_obj_new_int(class_instance->a + class_instance->b);
}
MP_DEFINE_CONST_FUN_OBJ_1(simpleclass_add_obj, simpleclass_add);
STATIC const mp_map_elem_t simpleclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_simpleclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_myclass), (mp_obj_t)&simpleclass_myclass_type },
{ MP_OBJ_NEW_QSTR(MP_QSTR_add), (mp_obj_t)&simpleclass_add_obj },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_simpleclass_globals,
simpleclass_globals_table
);
const mp_obj_module_t simpleclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_simpleclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_simpleclass, simpleclass_user_cmodule, MODULE_SIMPLECLASS_ENABLED);
One more thing to note: the functions that are pointed to in
simpleclass_myclass_type
are not registered with the macro
MP_DEFINE_CONST_FUN_OBJ_VAR
or similar. The reason for this is that
this happens automatically: myclass_print
does not require
user-supplied arguments beyond self
, so it is known what the
signature should look like. In myclass_make_new
, we inspect the
argument list, when calling
mp_arg_check_num(n_args, n_kw, 2, 2, true);
so, again, there is no need to turn our function into a function object.
Printing class properties¶
In my_print
, instead of the standard the C function printf
, we
made use of mp_print_str
, and mp_obj_print_helper
, which are
options in this case. Both take print
as their first argument. The
value of print
is supplied by the .print
method of the class
itself. The second argument is a string (in the case of
mp_print_str
), or a micropython
object (for
mp_obj_print_helper
). In addition, mp_obj_print_helper
takes a
pre-defined constant, PRINT_REPR
as its third argument. By resorting
to these micropython
printing functions, we can make certain that
the output is formatted nicely, independent of the platform. Whenever
print
is available, these function should be used instead of
printf
. For debugging purposes, printf
is also fine. More on the
subject can be found in mpprint.c
.
https://github.com/v923z/micropython-usermod/tree/master/snippets/simpleclass/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/simpleclass.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SIMPLECLASS_ENABLED=1 all
%%micropython
import simpleclass
a = simpleclass.myclass(2, 3)
print(a)
print(a.mysum())
myclass(2, 3)
5
Special methods of classes¶
Python has a number of special methods, which will make a class behave
as a native object. So, e.g., if a class implements the
__add__(self, other)
method, then instances of that class can be
added with the +
operator. Here is an example in python:
class Adder:
def __init__(self, value):
self.value = value
def __add__(self, other):
self.value = self.value + other.value
return self
a = Adder(1)
b = Adder(2)
c = a + b
c.value
3
Note that, while the above example is not particularly useful, it proves
the point: upon calling the +
operator, the values of a
, and
b
are added. If we had left out the implementation of the
__add__
method, the python interpreter would not have a clue as to
what to do with the objects. You can see for yourself, how sloppiness
makes itself manifest:
class Adder:
def __init__(self, value):
self.value = value
a = Adder(1)
b = Adder(2)
c = a + b
c.value
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-77-635006a6f7bc> in <module>
7 b = Adder(2)
8
----> 9 c = a + b
10 c.value
TypeError: unsupported operand type(s) for +: 'Adder' and 'Adder'
Indeed, we do not support the +
operator.
Now, the problem is that in the C implementation, these special methods have to be treated in a special way. The naive approach would be to add the pointer to the function to the locals dictionary as
STATIC const mp_rom_map_elem_t simpleclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___add__), MP_ROM_PTR(&simpleclass_add_obj) },
};
but that would not work. Well, this is not entirely true: the +
operator would not work, but one could still call the method explicitly
as
a = Adder(1)
b = Adder(2)
a.__add__(b)
Before we actually add the +
operator to our class, we should note
that there are two kinds of special methods, namely the unary and the
binary operators.
In the first group are those, whose sole argument is the class instance
itself. Two frequently used cases are the length operator, len
, and
bool
. So, e.g., if your class implements the __len__(self)
method, and the method returns an integer, then you can call the len
function in the console
len(myclass)
In the second category of operators are those, which require a left, as
well as a right hand side: the operand on the left hand side is the
class instance itself, while the right hand side can, in principle, be
another instance of the same class, or some other type. An example for
this was the __add__
method in our Adder
class. To prove that
the right hand side needn’t be of the same type, think of the
multiplication of lists:
[1, 2, 3]*5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
is perfectly valid, and has a well-defined meaning. It is the
responsibility of the C implementation to inspect the right hand side,
and decide how to interpret the operation. The complete list of unary,
as well as binary operators can be found in runtime.h
.
The module below implements five special methods altogether. Two unary,
namely, bool
, and len
, and three binary operators, ==
,
+
, and *
. Since the addition and multiplication will return a
new instance of specialclass_myclass
, we define a new function,
create_new_class
, that, well, creates a new instance of
specialclass_myclass
, and initialises the members with the two input
arguments. This function will also be called in the class initialisation
function, myclass_make_new
, immediately after the argument checking.
When implementing the operators, we have to keep a couple of things in
mind. First, the specialclass_myclass_type
has to be extended with
the two methods, .unary_op
, and .binary_op
, where .unary_op
is equal to the function that handles the unary operation
(specialclass_unary_op
in the example below), and .binary_op
is
equal to the function that deals with binary operations
(specialclass_binary_op
below). These two functions have the
signatures
STATIC mp_obj_t specialclass_unary_op(mp_unary_op_t op, mp_obj_t self_in)
and
STATIC mp_obj_t specialclass_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs)
respectively, and we have to inspect the value of op
in the
implementation. This is done in the two switch
statements.
Second, if .unary_op
, or .binary_op
are defined for the class,
then the handler function must have an implementation of all possible
operators. This doesn’t necessarily mean that you have to have all cases
in the switch
, but if you haven’t, then there must be a default
case with a reasonable return value, e.g., MP_OBJ_NULL
, or
mp_const_none
, so as to indicate that that particular method is not
available.
https://github.com/v923z/micropython-usermod/tree/master/snippets/specialclass/specialclass.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
#include "py/binary.h"
typedef struct _specialclass_myclass_obj_t {
mp_obj_base_t base;
int16_t a;
int16_t b;
} specialclass_myclass_obj_t;
const mp_obj_type_t specialclass_myclass_type;
STATIC void myclass_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
specialclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
mp_print_str(print, "myclass(");
mp_obj_print_helper(print, mp_obj_new_int(self->a), PRINT_REPR);
mp_print_str(print, ", ");
mp_obj_print_helper(print, mp_obj_new_int(self->b), PRINT_REPR);
mp_print_str(print, ")");
}
mp_obj_t create_new_myclass(uint16_t a, uint16_t b) {
specialclass_myclass_obj_t *out = m_new_obj(specialclass_myclass_obj_t);
out->base.type = &specialclass_myclass_type;
out->a = a;
out->b = b;
return MP_OBJ_FROM_PTR(out);
}
STATIC mp_obj_t myclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 2, 2, true);
return create_new_myclass(mp_obj_get_int(args[0]), mp_obj_get_int(args[1]));
}
STATIC const mp_rom_map_elem_t myclass_locals_dict_table[] = {
};
STATIC MP_DEFINE_CONST_DICT(myclass_locals_dict, myclass_locals_dict_table);
STATIC mp_obj_t specialclass_unary_op(mp_unary_op_t op, mp_obj_t self_in) {
specialclass_myclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
switch (op) {
case MP_UNARY_OP_BOOL: return mp_obj_new_bool((self->a > 0) && (self->b > 0));
case MP_UNARY_OP_LEN: return mp_obj_new_int(2);
default: return MP_OBJ_NULL; // operator not supported
}
}
STATIC mp_obj_t specialclass_binary_op(mp_binary_op_t op, mp_obj_t lhs, mp_obj_t rhs) {
specialclass_myclass_obj_t *left_hand_side = MP_OBJ_TO_PTR(lhs);
specialclass_myclass_obj_t *right_hand_side = MP_OBJ_TO_PTR(rhs);
switch (op) {
case MP_BINARY_OP_EQUAL:
return mp_obj_new_bool((left_hand_side->a == right_hand_side->a) && (left_hand_side->b == right_hand_side->b));
case MP_BINARY_OP_ADD:
return create_new_myclass(left_hand_side->a + right_hand_side->a, left_hand_side->b + right_hand_side->b);
case MP_BINARY_OP_MULTIPLY:
return create_new_myclass(left_hand_side->a * right_hand_side->a, left_hand_side->b * right_hand_side->b);
default:
return MP_OBJ_NULL; // operator not supported
}
}
const mp_obj_type_t specialclass_myclass_type = {
{ &mp_type_type },
.name = MP_QSTR_specialclass,
.print = myclass_print,
.make_new = myclass_make_new,
.unary_op = specialclass_unary_op,
.binary_op = specialclass_binary_op,
.locals_dict = (mp_obj_dict_t*)&myclass_locals_dict,
};
STATIC const mp_map_elem_t specialclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_specialclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_myclass), (mp_obj_t)&specialclass_myclass_type },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_specialclass_globals,
specialclass_globals_table
);
const mp_obj_module_t specialclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_specialclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_specialclass, specialclass_user_cmodule, MODULE_SPECIALCLASS_ENABLED);
https://github.com/v923z/micropython-usermod/tree/master/snippets/specialclass/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/specialclass.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_SPECIALCLASS_ENABLED=1 all
%%micropython
import specialclass
a = specialclass.myclass(1, 2)
b = specialclass.myclass(10, 20)
print(a)
print(b)
print(a + b)
myclass(1, 2)
myclass(10, 20)
myclass(11, 22)
Properties¶
In addition to methods, in python, classes can also have properties, which will basically return some read-only attributes of the class. Take the following example:
class PropClass:
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
We can now create an instance of PropClass
, and access the value of
_x
by “calling” the decorated x
method without the brackets
characteristic of function calls:
c = PropClass(12.3)
c.x
12.3
One use case is, when you want to protect the value of _x
, and want
to prevent accidental changes: if you want to write to the x
property, you’ll get a nicely-formatted exception:
c.x = 55.5
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-50-63b5601caccb> in <module>
----> 1 c.x = 55.5
AttributeError: can't set attribute
It is nifty, isn’t it? Now, let us see, how we can deal with this in micropython. In order to simplify things, we will implement what we have just seen above: a class that holds a single floating point value, and does nothing else.
Most of the code should be familiar from our first example on classes, so I will discuss the single new function that is relevant to properties. At the C level, a property is nothing but a void function with exactly three arguments
STATIC void some_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
...
}
where self_in
is the class instance, attribute
is a string with
the property’s name, and destination
is a pointer to the return
value of the function that is going to be called, when querying for the
property. So, in the python example above, attribute
is x
,
because we queried the x
property, and destination
will be the
equivalent of self._x
, because self._x
is what is returned by
the PropClass.x()
method.
In the C function, we do not return anything, instead, we assign the
desired property (attribute) of the class to destination[0]
as in
the snippet below:
STATIC void propertyclass_attr(mp_obj_t self, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self);
}
}
The qstr
is required, because a class might have multiple
properties, but all these properties are retrieved by a single function,
propertyclass_attr
. Thus, should we want to return another property
with name y
, we would augment the function as
STATIC void propertyclass_attr(mp_obj_t self, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self);
} else if(attribute == MP_QSTR_y) {
destination[0] = propertyclass_y(self);
}
}
Now, we are almost done, but we still have to implement the function that actually retrieves the attribute. This is what happens here:
STATIC mp_obj_t propertyclass_x(mp_obj_t self_in) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_float(self->x);
}
Remember, destination
was a pointer to mp_obj_t
, so whatever
function we have, it must return mp_obj_t
. In this particular case,
the implementation is trivial: we fetch the value of self->x
, and
turn it into an mp_obj_new_float
.
We are now done, right? Not quite: while the required functions are
implemented, they will never be called. We have to attach them to the
class, so that the interpreter knows what is to do, when we try to
access c.x
. This act of attaching the function happens in the type
definition of the class: we equate the .attr
member of the structure
with our propertyclass_attr
functions, so that the interpreter can
fill in the three arguments.
And with that, we are ready to compile the code.
https://github.com/v923z/micropython-usermod/tree/master/snippets/properties/properties.c
#include <stdio.h>
#include "py/runtime.h"
#include "py/obj.h"
typedef struct _propertyclass_obj_t {
mp_obj_base_t base;
mp_float_t x;
} propertyclass_obj_t;
const mp_obj_type_t propertyclass_type;
STATIC mp_obj_t propertyclass_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
mp_arg_check_num(n_args, n_kw, 1, 1, true);
propertyclass_obj_t *self = m_new_obj(propertyclass_obj_t);
self->base.type = &propertyclass_type;
self->x = mp_obj_get_float(args[0]);
return MP_OBJ_FROM_PTR(self);
}
STATIC mp_obj_t propertyclass_x(mp_obj_t self_in) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
return mp_obj_new_float(self->x);
}
MP_DEFINE_CONST_FUN_OBJ_1(propertyclass_x_obj, propertyclass_x);
STATIC const mp_rom_map_elem_t propertyclass_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_x), MP_ROM_PTR(&propertyclass_x_obj) },
};
STATIC MP_DEFINE_CONST_DICT(propertyclass_locals_dict, propertyclass_locals_dict_table);
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self_in);
}
}
const mp_obj_type_t propertyclass_type = {
{ &mp_type_type },
.name = MP_QSTR_propertyclass,
.make_new = propertyclass_make_new,
.attr = propertyclass_attr,
.locals_dict = (mp_obj_dict_t*)&propertyclass_locals_dict,
};
STATIC const mp_map_elem_t propertyclass_globals_table[] = {
{ MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_propertyclass) },
{ MP_OBJ_NEW_QSTR(MP_QSTR_propertyclass), (mp_obj_t)&propertyclass_type },
};
STATIC MP_DEFINE_CONST_DICT (
mp_module_propertyclass_globals,
propertyclass_globals_table
);
const mp_obj_module_t propertyclass_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t*)&mp_module_propertyclass_globals,
};
MP_REGISTER_MODULE(MP_QSTR_propertyclass, propertyclass_user_cmodule, MODULE_PROPERTYCLASS_ENABLED);
Before we compile the module, I would like to add two comments to what was said above.
First, in the function that we assigned to .attr
,
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
destination[0] = propertyclass_x(self_in);
}
}
we called a function on self_in
, propertyclass_x()
, and assigned
the results to destination[0]
. However, this extra trip is not
absolutely necessary: we could have equally done something along these
lines:
STATIC void propertyclass_attr(mp_obj_t self_in, qstr attribute, mp_obj_t *destination) {
if(attribute == MP_QSTR_x) {
propertyclass_obj_t *self = MP_OBJ_TO_PTR(self_in);
destination[0] = mp_obj_new_float(self->x);
}
}
The case in point being that destination[0]
is simply an
mp_obj_t
object, it does not matter, where and how it is produced.
Since self
is available to propertyclass_attr
, if the property
is simple, as above, one can save the function call, and do everything
in place.
Second, more examples on implementing properties can be found in
py/profile.c.
Just look for the .attr
string, and the associated functions!
https://github.com/v923z/micropython-usermod/tree/master/snippets/properties/micropython.mk
USERMODULES_DIR := $(USERMOD_DIR)
# Add all C files to SRC_USERMOD.
SRC_USERMOD += $(USERMODULES_DIR)/properties.c
CFLAGS_USERMOD += -I$(USERMODULES_DIR)
!make clean
!make USER_C_MODULES=../../../usermod/snippets CFLAGS_EXTRA=-DMODULE_PROPERTYCLASS_ENABLED=1 all
%%micropython
import propertyclass
a = propertyclass.propertyclass(12.3)
print(a.x)
12.3