Object orientated programming

Object orientated programming#

Python is an object orientated programming language. We’ve used functions to make code we write re-usable in other projects. Often those functions are used to very specifically group an algorithm that solves a certain problem, like sorting for example. A first step is to wrap this algorithm into a block of code and give it a name. That’s what the function definition actually does. And it will generate an interface for using the algorithm in a specific way by the parameters it accepts and the return values it provides. After that you probably reach the point at some point, where you would love to use the named algorithm for a slightly different, but very comparable problem. Imagine you had to sort numbers before and are now up to sorting names in alphabetical order. So you will want to generalize your algorithm and function call to handle both and you’re fine afterwards.

Object orientated programming aims for going one step further. Instead of grouping lines of code to a named algorithm, it groups all related named algorithms and the corresponding data into a new object. This is also a change in perspective. Before we had functions matching to data, now we have objects bringing both the data an the functionality to work with it.

Let’s use an image to make the idea clearer. Think of describing some basic properties of a car. It will have four wheels, a motor, a battery, a steering wheel, some seats and so on. Furthermore that car can do things. It can drive forward and backward, to the left and to the right. It can accelerate and slow down and so forth. It is very straight forward to describe a car like that. Thinking of the object, which is the car, the properties it has, the data, and the things you can do with it, the functionality.

That’s the basics behind object orientated programming. Let’s have a look how this is used in Python.

The most minimal definition of an object is a one liner in Python. We define a class called MyClass here, which doesn’t implement anything new, nor has it any data related to it. It can be directl used however.

class MyClass:
    pass
my_class = MyClass()

Note

Class names are usually specified in CamelCase, where every syllable starts with a capital letter. This makes the use of classes very recognizable.

Using our well known dir functions reveals that our new simple class is already capable of a few things, or at least feels prepared for being used. Quite some of those dunder methods are already available for use.

dir(my_class)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

Tip

Play around with those dunder methods a bit to see the intention behind them.

Group functionality#

Now let’s create a more complex example to make the grouping aspect more obvious. Let’s begin to create some geometry handling classes. Why not start with a point representation?

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"a point at ({self.x},{self.y})"

Okay. So quite a few new things are visible here. First of all there is the class definition for a class called Point. Following are two functions __init__ and __repr__. They’re indented by one level. In Pythons logic that alone makes them belong to the class, as that creates a new block, which ends with last line of our class definition. All functions belonging to a class context are called methods from now on.

Next there is an unusual and unexpected first parameter to both methods called self. This is a specific class related parameter, which allows for binding functionality and data to the class. Think of it as the glue that holds everything together. Everything that is bound to this special variable will be accessible throughout the class. The name for this variable can be freely used just as every other variable name in Python can as well. It is a convention however to name this variable self. It’s very advisable to keep this name as it makes code much easier to recognize and understand for other readers. So, better don’t feel tempted to play with it.

The __init__ method has two further parameters, x and y. Or more specifically it has just two parameters, as the self parameter, specified as the first parameter to any method in a class, doesn’t count as a parameter. Another difference when comparing methods and functions. The __init__ method is special. It’s called the constructor of the class. In it we can implement all functionality which should be executed when the class is created, more specifically an Instance of it is created. In this case, we assign the values of the parameters x and y to attributes of the class with the same name self.x and self.y making those values available throughout the class.

The __repr__ method allows to specify a string representation of the class, which is used for example when an Instance of this class is printed. Let’s play with that and create a point in the origin of a two dimensional coordinate system and print it.

p1 = Point(0, 0)
print(p1)
a point at (0,0)

Quite nice actually. Let’s go a step further and create a class representing a line. This line will consist of two points, and instead of somehow using plain coordinates to specify the line, we’ll be using two point objects to specify it. Additionally we will add a method to retrieve the length of that line.

import math


class Line:
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2

    def length(self):
        return math.sqrt((self.p2.y - self.p1.y) ** 2 + (self.p2.x - self.p1.x) ** 2)

    def __repr__(self):
        return f"a line from {self.p1} to {self.p2}"

Now we can instantiate a second Point, acting as the lines end point and create the line.

p2 = Point(0, 1)
l = Line(p1, p2)
print(l)
a line from a point at (0,0) to a point at (0,1)

And of course we can print the lines length now.

print(f"The length of the line is {l.length()} units.")
The length of the line is 1.0 units.

Following that scheme we can create ever more complex objects. A triangle for example could be implemented like this. A length method wouldn’t make sense here. A circumference on the other hand would be nice though. And to calculate the circumference, we can make use of our Line objects and its length method.

class Triangle:
    def __init__(self, p1, p2, p3):
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
        
    def circumference(self):
        l1 = Line(self.p1, self.p2).length()
        l2 = Line(self.p2, self.p3).length()
        l3 = Line(self.p3, self.p1).length()
        
        return l1 + l2 + l3

    def __repr__(self):
        return f"a triangle based on {self.p1}, {self.p2} and {self.p3}."
p3 = Point(1, 1)
t = Triangle(p1, p2, p3)
t
a triangle based on a point at (0,0), a point at (0,1) and a point at (1,1).
print(f"The circumference of the triangle is {t.circumference()} units.")
The circumference of the triangle is 3.414213562373095 units.

What about a rectangle. Pretty straightforward I think.

class Rectangle:
    def __init__(self, p1, p2, p3, p4):
        self.p1 = p1
        self.p2 = p2
        self.p3 = p3
        self.p4 = p4
        
    def circumference(self):
        l1 = Line(self.p1, self.p2).length()
        l2 = Line(self.p2, self.p3).length()
        l3 = Line(self.p3, self.p4).length()
        l4 = Line(self.p4, self.p1).length()
        
        return l1 + l2 + l3 + l4

    def __repr__(self):
        return f"a rectangle based on {self.p1}, {self.p2}, {self.p3} and {self.p4}."
p4 = Point(1, 0)
r = Rectangle(p1, p2, p3, p4)
r
a rectangle based on a point at (0,0), a point at (0,1), a point at (1,1) and a point at (1,0).
print(f"The circumference of the rectangle is {r.circumference()} units.")
The circumference of the rectangle is 4.0 units.

And here’s a final example for geometries. A different implementation for a Rectangle. It will be defined by two opposite points, and have a method to get the circumference and the area of the square. We’ll be re-using as much as possible for the implementation. To ease the implementation of the circumference method based on the two opposite points, we create the four corner points from them temporarily and use the strategy known from the Rectangle class to calculate the circumference.

class OP_Rectangle:
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2
        
    def circumference(self):
        _p1 = Point(self.p1.x, self.p1.y)
        _p2 = Point(self.p1.x, self.p2.y)
        _p3 = Point(self.p2.x, self.p2.y)
        _p4 = Point(self.p2.x, self.p1.y)
        
        l1 = Line(_p1, _p2).length()
        l2 = Line(_p2, _p3).length()
        l3 = Line(_p3, _p4).length()
        l4 = Line(_p4, _p1).length()
        
        return l1 + l2 + l3 + l4
    
    def area(self):
        return abs(self.p2.y - self.p1.y) * abs(self.p2.x - self.p1.x)
        
    def __repr__(self):
        return f"a rectangle defined by {self.p1} and {self.p2} being opposite to it."
r2 = OP_Rectangle(p1, p3)
print(r2)
a rectangle defined by a point at (0,0) and a point at (1,1) being opposite to it.
print(f"The circumference of the rectangle is {r2.circumference()} units.")
The circumference of the rectangle is 4.0 units.
print(f"The area of the rectangle is {r2.area()} square units.")
The area of the rectangle is 1 square units.

Inheritance#

Another important aspect of object orientated programming is inheritance. The idea is to create more complex, or more specific functionality based on a simpler one by deriving the new implementation from an existing one and replacing or adding necessary functionality to it.

The implementation for our Square class could be done much cleaner when using this concept. Let’s have a look.

class OP_Rectangle(Rectangle):
    def __init__(self, p1, p2):
        _p1 = Point(p1.x, p1.y)
        _p2 = Point(p1.x, p2.y)
        _p3 = Point(p2.x, p2.y)
        _p4 = Point(p2.x, p1.y)
        Rectangle.__init__(self, _p1, _p2, _p3, _p4)
        
    def area(self):
        return abs(self.p3.y - self.p1.y) * abs(self.p3.x - self.p1.x)
r2 = OP_Rectangle(p1, p3)
print(r2)
a rectangle based on a point at (0,0), a point at (0,1), a point at (1,1) and a point at (1,0).
print(f"The circumference of the rectangle is {r2.circumference()} units.")
The circumference of the rectangle is 4.0 units.
print(f"The area of the rectangle is {r2.area()} square units.")
The area of the rectangle is 1 square units.

The new code appears to be much cleaner, as it traces the new implementation back to an existing one, which is already understood and known.

All we had to do for that was to create the four necessary corner points for the existing Rectangle implementation and call its constructor like shown. This creates the four points self.p1, self.p2, self.p3 and self.p4 as properties of our OP_Rectangle class, so that its coordinates can later be used to calculate the rectangles area. If desired, we could of course also replace the __repr__ method to be more specific on the two initial points that had be used in the OP_Rectangles constructor.