Table Of Contents

Previous topic

Standard Library

Next topic

A Quick Note about Python 3

This Page

Object Oriented Programming

Python natively supports object oriented programming via classes and inheritance. In the course of this tutorial, we’ll right a very simply object that represents a number with a unit, showing some of the capabilities of OOP with Python. Below, it is shown how to implement string representations of a custom object, how to allow arithmic operations with your objects (+,-,*,**), how to allow for comparison and sorting, and how to add custom methods.

Some things that we will not cover but is nonetheless possible, is how make a class iterable, i.e. so you can do:

>>> for element in myclass:

You need to implement the __iter__ and __next__ method for this. Also, it is possible to provide a dictionary style access to a class, if you implement __getitem__ and __setitem__.

Initialization

The minimal code to write a class is simply:

class Unit:
    pass

However, it is recommended to always subclass the basic Python object object, sot that you get automatic access to some of its useful features (as will be shown below). Inheritance is simply done by passing the object from which you want to inherit all methods and attributes as an argument to the class statement. We’ll also immediately add some documentation to the class definition:

class Unit(object):
    """Object representing a number with a unit."""
    pass

This object can be simply initiated via:

>>> myunit = Unit()
>>> print(myunit)
<__main__.Unit instance at 0x2f1cab8>

But of course it doesn’t do very much. We would rather like to give a value and a unit whenever we instantiate a Unit object:

myunit = Unit(7., 'm')

To get this behaviour, we need to implement the __init__ function, that gets called whenever an object is initialized:

class Unit(object):
    """Object representing a number with a unit."""

    def __init__(self, value, unit):
        self.value = value
        self.unit = unit
        other_variable = 10.

From now on, we will not right the entire class definition again for every function we add, just remember that they are all added on the same indentation level as this __init__ function.

A couple of remarks are in place here:

  • the double leading and trailing underscores denote that the function names are Python built-ins. Never use them for other purposes than they are meant to.

  • You can see three arguments in the init function, but we only wanted two (the value and the unit). For the current purposes, the first argument to a class function needs to be self. It is a variable denoting the class itself. Via the call:

    self.value = value
    

    we transform the value variable from a local variable (i.e. only visible within the init function itself, to a class variable (accessible anywhere). Note the difference between value and other_variable:

    >>> myunit = Unit(7., 'm')
    >>> print(myunit.value)
    7.0
    >>> print(myunit.other_variable)
    AttributeError                            Traceback (most recent call last)
    <ipython-input-12-c7267ea0be2a> in <module>()
    ----> 1 x.other_variable
    AttributeError: 'Unit' object has no attribute 'other_variable'
    

String representations

Another thing we want to fix, is having a nice string representation for the object when we call print(myunit) or repr(myunit):

def __str__(self):
    print('{} {}'.format(self.value, self.unit))

def __repr__(self):
    print("Unit({}, '{}')".format(self.value, self.unit))

Now we get the following behaviour:

>>> myunit = Unit(7., 'm')
>>> print(myunit)
7.0 m
>>> repr(myunit)
"Unit(7.0, 'm')"

The latter is meant as more machine-readable string representation, and always tries to aim at getting the eval function as the ‘inverse’ function (though certainly for complicated classes, this is not always possible):

>>> myunit2 = eval(repr(myunit))
>>> myunit.value
7.0

Comparisons

Another thing that might be useful with units, is begin able to compare them. This can be done via implementing __lt__ (less-than) operator, and the __eq__ (equality) operator:

def __lt__(self, other):
    if self.unit == other.unit:
        return self.value < other.value
    else:
        raise ValueError("Cannot compare values with different units")

def __eq__(self, other):
    if self.unit == other.unit:
        return self.value == other.value
    else:
        raise ValueError("Cannot compare values with different units")

Now you can compare numbers with (equal) units:

>>> u1 = Unit(7.,'m')
>>> u2 = Unit(6.,'m')
>>> u3 = Unit(7.,'m')
>>> u4 = Unit(6.,'cm')
>>> print(u1==u2)
False
>>> print(u1>u2)
True

And if they are inside a list, you can sort them:

>>> mylist = [u1, u2]
>>> print(mylist)
[Unit(7.0, 'm'), Unit(6.0, 'm')]
>>> print(sorted(mylist))
[Unit(6.0, 'm'), Unit(7.0, 'm')]

Basic arithmics

Unit classes are of little use if you cannot calculate with them. The built-in Python arithmics can be easily implemented via __add__, __sub__, __mul__, __div__ and __pow__:

def __add__(self, other):
    if self.unit == other.unit:
        return Unit(self.value, other.value, unit)
    else:
        raise ValueError("Cannot add values with different units")

Then you can do:

>>> u1 = Unit(7.,'m')
>>> u2 = Unit(6.,'m')
>>> print(u1+u2)
13.0 m

Numpy arithmics

Perhaps you also want to allow numpy to take log10, or sin of your units. In this case, you need to add these functions to your class. This should be proper methods of the class, i.e. the shouldn’t contain the leading and trailing double underscores:

def log10(self):
    return Unit(np.log10(self.value),'')

Notice that the log10 function does not return a value, and in fact you could argue that it should also check if the unit is the empty string, since you cannot take the log10 of a unit. Now, you are able to do:

>>> u1 = Unit(7., 'm')
>>> print(np.log10(u1))
0.845098040014

Note that you can actually put them in an array!

>>> u1 = Unit(7., 'm')
>>> u2 = Unit(6., 'm')
>>> units = np.array([u1, u2])
>>> print(np.log10(units))
[Unit(0.845098040014, '') Unit(0.778151250384, '')]
Copyright: Smithsonian Astrophysical Observatory under terms of CC Attribution 3.0 Creative Commons
 License