Object-oriented programming is a widespread paradigm that helps programmers create intuitive layers of abstraction. This example notebook is aimed at helping people along with the concepts of object-oriented programming and may require more time than is possible in a one-day workshop.
In Python, classes are defined with the keyword class. All classes should inherit either some other class or a base-class confusingly called object.
When a function is associated with a class, it is called a method.
In [ ]:
class Vector2D(object):
"""Represents a 2-dimensional vector"""
def __init__(self, x, y):
self.x = x
self.y = y
Typically classes are made to store some data that is logically related to a single entity.
When a class is instantiated, i.e. given a set of values , the result of the action is an object.
In [ ]:
vector1 = Vector2D(1,1)
vector2 = Vector2D(0,0)
type(vector1)(2, 3)
Object-oriented design can let programmers re-use code by inheriting other classes to extend their functionality.
In [ ]:
class AddableVector2D(Vector2D):
"""Represents a 2D vector that can be added to another"""
def add(self, another):
return type(self)(self.x + another.x, self.y + another.y)
In [ ]:
addvec1 = AddableVector2D(1,1)
addvec2 = AddableVector2D(2,2)
addvec3 = addvec1.add(addvec2)
Python has plenty of so-called magic methods for classes. For instance, the __str__ method defines how a vector is textually represented when you call it's str. The __repr__ controls how the object is represented in the interpreter.
In [ ]:
class RepresentableVector2D(Vector2D):
""" A vector that has textual representations"""
def __str__(self):
return "Vector2D({},{})".format(self.x, self.y)
def __repr__(self):
return str(self)
In [ ]:
rv1 = RepresentableVector2D(1, 2)
print(str(rv1))
rv1
Python supports multiple inheritance. This is another very powerful tool, but can sometimes lead to confusion when the same function is defined in many classes. If this confuses you, the concept to look for is Method Resolution Order .
It is possible to call a parent class's methods using the keyword super.
Also, it is possible to define what regular operators do to a class.
In [ ]:
class ExtendedVector2D(AddableVector2D, RepresentableVector2D):
""" A vector that has several features"""
def __str__(self):
return "Extended" + super(ExtendedVector2D, self).__str__()
# addition with +
def __add__(self, other):
return self.add(other)
# negation with - prefix
def __neg__(self):
return type(self)(-self.x, -self.y)
# subtraction with -
def __sub__(self, other):
return self + (-other)
def __mul__(self, scalar):
return type(self)(self.x*scalar, self.y*scalar)
In [ ]:
vec1 = ExtendedVector2D(1, 4)
vec2 = vec1 * 0.5
vec3 = vec1 - (vec2*3)
vec3
Observe that in most cases we don't check for the types of the arguments given. This is idiomatic Python and contrary to some other object-oriented languages.
It is better to ask for forgiveness than permission
Another powerful feature of classes is composability: you can abstract something to another level.
Let's make a class to represent a very rough vector-based drawing.
In [ ]:
class Drawing2D(object):
def __init__(self, x=0, y=0, initial_vectors=None):
self.x = x
self.y = y
self.vectors = initial_vectors if initial_vectors else []
def add(self, vector):
self.vectors.append(vector)
def scale(self, scalar):
self.vectors = [v*scalar for v in self.vectors]
def __str__(self):
output = "start at {},{}.\n".format(self.x, self.y)
for vector in self.vectors:
output += "draw vector {},{}\n".format(vector.x, vector.y)
return output
As in all Python one should be aware of when an operation changes the state of an object, like our drawing and when it returns a new object, like multiplying vectors with a scalar.
Instead of text we could create a Scalable Vector Graphics (SVG) image using this class.
In [ ]:
vectors = [vec1, vec2, vec3]
drawing = Drawing2D(0,0, vectors)
print(drawing)
drawing.scale(0.75)
print('---')
print(drawing)
print('---')
drawing.add(ExtendedVector2D(-1,-1))
print(drawing)
In Python, exceptions are lightweight, i.e. handling them doesn't cause a notable decrease in performance as happens in some languages.
The purpose of exceptions is to communicate that something didn't go right. The name of the exception typically tells what kind of error ocurred and the exception can also contain a more explicit message.
In [ ]:
class Container(object):
def __init__(self):
self.bag = {}
def put(self, key, item):
self.bag[key] = item
def get(self, key):
return self.bag[key]
The container-class can exhibit at least two different exceptions.
In [ ]:
container = Container()
container.put([1, 2, 3], "example")
In [ ]:
container.get("not_in_it")
Who should worry about the various issues is a good philosophical question. We could either make the Container-class secure in that it doesn't raise any errors to whoever calls it or we could let the caller worry about such errors.
For now let's assume that the programmer is competent and knows what is a valid key and what isn't.
In [ ]:
try:
container = Container()
container.put([1,2,3], "value")
except TypeError as err:
print("Stupid programmer caused an error: " + str(err))
A try-except may contain a finallyblock, which is always guaranteed to execute.
Also, it is permissible to catch multiple different errors.
In [ ]:
try:
container = Container()
container.put(3, "value")
container.get(3)
except TypeError as err:
print("Stupid programmer caused an error: " + str(err))
except KeyError as err:
print("Stupid programmer caused another error: " + str(err))
finally:
print("all is well in the end")
# go ahead, make changes that cause one of the exceptions to be raised
There is also syntax for catching multiple error types in the same catch clause.
The keyword raise is used to continue error handling. This is useful if you want to log errors but let them pass onward anyway.
A raise without arguments will re-raise the error that was being handled.
In [ ]:
try:
container = Container()
container.put(3, "value")
container.get(5)
except (TypeError, KeyError) as err:
print("please shoot me")
if type(err) == TypeError:
raise Exception("That's it I quit!")
else:
raise