name = '2016-05-06-classes'
title = 'Python basics: classes'
tags = 'basics, oop'
author = 'Denis Sergeev'
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.
Python supports object-oriented programming (OOP). The goals of OOP are [2]:
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.
For example, let's create a string:
s = 'hello world'
Check its type:
type(s)
str
Then, using dir()
function, we can print out all the methods of a str
object.
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
Numpy arrays are a specific class as well, with a bunch of array-specific methods and attributes.
import numpy as np
a = np.zeros((10,10))
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']
The simplest way to create a class:
class MyAwesomeClass:
pass
Note: According to PEP8, class names should normally use the CapWords convention.
Now, create a variable using the just created class:
c = MyAwesomeClass()
c
<__main__.MyAwesomeClass at 0x7f3be00a2c50>
Let's define a bit more useful, but still a very simple class [3].
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
)
tiger = Creature('big evil tiger', 21)
tiger
Creature: big evil tiger of level 21
Note the difference in the output after we added a custom __repr__
method to our class.
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]:
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)
F = Dog('Fido')
B = Dog('Buddy')
F.add_trick('roll over')
B.add_trick('play dead')
F.tricks
['roll over', 'play dead']
Correct design of the class should use an instance variable instead:
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)
F = Dog('Fido')
B = Dog('Buddy')
F.add_trick('roll over')
B.add_trick('play dead')
F.tricks
['roll over']
B.tricks
['play dead']
The best way to understand the logic and convenience of OOP is by examining many examples of its use.
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:
u = Vec2D(0,1)
v = Vec2D(1,0)
w = Vec2D(1,1)
a = u + v
print(a)
this is vector components: 1 1
a == w
False
a = u * v
print(a)
0
u == v
False
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__
.
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)
The bot in action: https://twitter.com/AtmosSciBot
Source code: https://github.com/dennissergeev/atmosscibot
[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
HTML(html)
This post was written as an IPython (Jupyter) notebook. You can view or download it using nbviewer.