In [1]:
name = '2016-05-06-classes'
title = 'Python basics: classes'
tags = 'basics, oop'
author = 'Denis Sergeev'

In [2]:
from nb_tools import connect_notebook_to_post
from IPython.core.display import HTML

html = connect_notebook_to_post(name, title, tags, author)

We start with the introduction from Python docs [1]

Compared with other programming languages, Python’s class mechanism adds classes with a minimum of new syntax and semantics. It is a mixture of the class mechanisms found in C++ and Modula-3. Python classes provide all the standard features of Object Oriented Programming: the class inheritance mechanism allows multiple base classes, a derived class can override any methods of its base class or classes, and a method can call the method of a base class with the same name. Objects can contain arbitrary amounts and kinds of data. As is true for modules, classes partake of the dynamic nature of Python: they are created at runtime, and can be modified further after creation.

OOP

Python supports object-oriented programming (OOP). The goals of OOP are [2]:

  • to organize the code, and
  • to re-use code in similar contexts.

Examples of classes

Using Python, you inevitably run into using classes, even if you don't create one yourself. Every object in Python is defined by its class and has class-specific attributes and methods.

Strings

For example, let's create a string:


In [3]:
s = 'hello world'

Check its type:


In [4]:
type(s)


Out[4]:
str

Then, using dir() function, we can print out all the methods of a str object.


In [5]:
print(dir(s))


['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

Note: a convenience to pronounce __add__ is "dunder add", where dunder stands for double underscore

Arrays

Numpy arrays are a specific class as well, with a bunch of array-specific methods and attributes.


In [6]:
import numpy as np

In [7]:
a = np.zeros((10,10))

In [8]:
print(dir(a))


['T', '__abs__', '__add__', '__and__', '__array__', '__array_finalize__', '__array_interface__', '__array_prepare__', '__array_priority__', '__array_struct__', '__array_wrap__', '__bool__', '__class__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__delitem__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__iand__', '__ifloordiv__', '__ilshift__', '__imatmul__', '__imod__', '__imul__', '__index__', '__init__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__xor__', 'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view']

Defining a class

The simplest way to create a class:


In [9]:
class MyAwesomeClass:
    pass

Note: According to PEP8, class names should normally use the CapWords convention.

Now, create a variable using the just created class:


In [10]:
c = MyAwesomeClass()

In [11]:
c


Out[11]:
<__main__.MyAwesomeClass at 0x7f3be00a2c50>

Let's define a bit more useful, but still a very simple class [3].


In [12]:
class Creature:
    def __init__(self, name, the_level):
        self.name = name
        self.level = the_level
    def __repr__(self):
        return "Creature: {} of level {}".format(
            self.name, self.level
        )

In [13]:
tiger = Creature('big evil tiger', 21)

In [14]:
tiger


Out[14]:
Creature: big evil tiger of level 21

Note the difference in the output after we added a custom __repr__ method to our class.

Caution about using mutable objects

Shared data can have possibly surprising effects with involving mutable objects such as lists and dictionaries. For example, the tricks list in the following code should not be used as a class variable because just a single list would be shared by all Dog instances [1]:


In [15]:
class Dog:

    tricks = [] # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

In [16]:
F = Dog('Fido')
B = Dog('Buddy')

In [17]:
F.add_trick('roll over')
B.add_trick('play dead')

In [18]:
F.tricks


Out[18]:
['roll over', 'play dead']

Correct design of the class should use an instance variable instead:


In [19]:
class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = [] # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

In [20]:
F = Dog('Fido')
B = Dog('Buddy')

In [21]:
F.add_trick('roll over')
B.add_trick('play dead')

In [22]:
F.tricks


Out[22]:
['roll over']

In [23]:
B.tricks


Out[23]:
['play dead']

More examples

The best way to understand the logic and convenience of OOP is by examining many examples of its use.

Class for vectors in the plane [4]


In [24]:
class Vec2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Vec2D(self.x + other.x, self.y + other.y)
    def __sub__(self, other):
        return Vec2D(self.x - other.x, self.y - other.y)
    def __mul__(self, other):
        return self.x*other.x + self.y*other.y
    def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2)
    def __str__(self):
        return 'this is vector components: {0:g} {1:g}'.format(self.x, self.y)
    def __ne__(self, other):
        return self.x != other.x or self.y != other.y

Let us play with some Vec2D objects:


In [25]:
u = Vec2D(0,1)
v = Vec2D(1,0)
w = Vec2D(1,1)

In [26]:
a = u + v

In [27]:
print(a)


this is vector components: 1 1

In [28]:
a == w


Out[28]:
False

In [29]:
a = u * v

In [30]:
print(a)


0

In [31]:
u == v


Out[31]:
False

Wind field instance

Let's look at another example that can be useful in Atmospheric and Oceanic sciences. This Wind3D instance is pretty simple, having only two specific methods beside the __init__.


In [32]:
class Wind3D(object):

    def __init__(self, u, v, w):
        """
        Initialize a Wind3D instance
        """
        if (u.shape != v.shape) or (u.shape != w.shape):
            raise ValueError('u, v and w must be the same shape')
        self.u = u.copy()
        self.v = v.copy()
        self.w = w.copy()
        
    def magnitude(self):
        """
        Calculate wind speed (magnitude of wind vector) and store it within the class
        """
        self.mag = np.sqrt(self.u**2 + self.v**2 + self.w**2)
    
    def kinetic_energy(self):
        """
        Calculate KE and return it
        """
        return 0.5*(self.u**2 + self.v**2 + self.w**2)

Other applications

Twitter bot example: AtmosSciBot

The bot in action: https://twitter.com/AtmosSciBot

References

[1] Python documentation: https://docs.python.org/3/tutorial/classes.html

[2] Short example from Scientific Python tutorial: http://www.scipy-lectures.org/intro/language/oop.html

[3] Python Jumpstart Course: https://github.com/mikeckennedy/python-jumpstart-course-demos

[4] H.P. Langtangen (2014) "A Primer on Scientific Programming with Python": http://hplgit.github.io/primer.html/doc/pub/half/book.pdf

Suggested reading


In [33]:
HTML(html)


Out[33]:

This post was written as an IPython (Jupyter) notebook. You can view or download it using nbviewer.