Object oriented programming

In the object oriented paradigm the data and the methods operating on them are put together into an object.

The blueprint for objects are classes.

Example: rotation of a set of points

Let’s see how it would look in both the procedural and the object oriented way

Procedural way

import numpy
points = [ [1, 2], [4,-2] ]
angle = numpy.pi/4 

def rotate(points, angle):
    cos = numpy.cos(angle)
    sin = numpy.sin(angle)
    rotated = []
    for x,y in points:
        newx = cos * x - sin * y 
        newy = sin * x + cos * y 
        rotated.append( [newx, newy] )
    return rotated

rotate(points, angle) 
[[-0.7071067811865474, 2.121320343559643],
 [4.242640687119286, 1.4142135623730947]]

Object oriented way

points = [ Point(1,2), Point(4,-2) ]
angle = numpy.pi/4 

for p in points:
    p.rotate\_by(angle) 
    
points
[Point(-0.7071067811865474,2.121320343559643),
 Point(4.242640687119286,1.4142135623730947)]

Point class definition

class Point():
    def \_\_init\_\_(self, x, y):
        self.x = x
        self.y = y
        
    def rotate\_by(self, angle):
        cos = numpy.cos(angle)
        sin = numpy.sin(angle)
        newx = cos * self.x - sin * self.y 
        newy = sin * self.x + cos * self.y 
        self.x = newx
        self.y = newy
        
    def \_\_str\_\_(self):
        return "({},{})".format(self.x, self.y)

    def \_\_repr\_\_(self):
        return "Point({},{})".format(self.x, self.y)
    
    

Ingredients of a python class

  • the constructor (\_\_init\_\_)
    • initialise the instance data
  • methods (rotate\_by)
    • act on the data
  • auxilliary methods
    • \_\_str\_\_
    • \_\_repr\_\_

All these functions

  • are defined inside the class block
  • take the class instance as their first argument

We will look at them in more detail now.

Constructor

The constructor is defined using the special function name \_\_init\_\_. Its first argument is the new instance, by convention called self, the other arguments are the arguments needed to set the instance state.

class Point():
    def \_\_init\_\_(self,x,y):
        self.x = x
        self.y = y

The data members are set by defining an attribute of self.

Member functions

Methods that act on the object data can be defined in the class block

class Point():
    # ...        
    def rotate\_by(self, angle):
        cos = numpy.cos(angle)
        sin = numpy.sin(angle)
        newx = cos * self.x - sin * self.y 
        newy = sin * self.x + cos * self.y 
        self.x = newx
        self.y = newy
        

The first argument is again self. The object data is referred to as an attribute of self.

Printing

class PrintDemo():
    def \_\_str\_\_(self):
        return "this is the \_\_str\_\_ method"
    def \_\_repr\_\_(self):
        return "this is the \_\_repr\_\_ method"
pd = PrintDemo()

The \_\_str\_\_ method tells python how to convert our class instances to a string. This method is used in the print function.

print(pd)
this is the \_\_str\_\_ method

The \_\_repr\_\_ function tells python how to represent the object. It is used in error messages or when the interpreter prints the result of a statement

pd
this is the \_\_repr\_\_ method

The difference between the two is not always very clear, and sometime the inplementation is the same, the rule of thumb is:

  • \_\_str\_\_ should be “nice” looking
  • \_\_repr\_\_ should have all the information needed to reconstruct the object

Inheritance

One of the most useful aspect of object oriented programming is the ability to have classes inherit from other classes to specialise behaviour.

class Integrator():
    def solve(self, f, x0, dt, nsteps):
        xs = numpy.linspace(nsteps+1)
        total = 0.0
        for i in range(nsteps):
            total += self.calculatePanel(f, xs[i], xs[i+1], dt)
        return total
    
class Euler(Integrator):
    def calculatePanel(self, f, xmin, xmax, dt):
        return f(xmin)*dt
    
class Midpoint(Integrator):
    def calculatePanel(self, f, xmin, xmax, dt):
        m = (xmin + xmax)/2
        return f(m)*dt

We can collect the common behavior in a base class (the Integrator class) and specialise the different methods in derived classes (Euler and Midpoint ).

More about classes

There is much more about classes that we did not cover:

  • destructor: some actions can be triggered when the instance of a class is deleted
  • operator overloading: you can define how class instances are added, subtracted, multiplied, …
  • class methods: you can define functions that are attached to the class rather than to an instance