.. contents:: :depth: 2 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, '')]