.. 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, '')]