What is Object Oriented Programming?
Object Oriented Programming (OOP) is a popular style of programming or paradigm. Programming paradigms are all about trying to make code easier to read and write. All code can be written in all paradigms and it will work just the same, but it may turn out more readable in one paradigm rather than another. Other programmers may find code easier to use and extend when written in a particular way. A clear style of programming can be particularly important when working in teams where several programmers work on a piece of code at the same time.
It is possible to program in OOP in many languages. Some languages provide specific features that make OOP easier but it is optional. Other languages specifically require an object oriented style and it is difficult to program in any other way. Some languages do not have all of the right features but it still possible to do object oriented programming to some extent. In Python OOP is optional and features are provided to support it. Programming without OOP in Python is known as a procedural style.
Say we want to write a program to get the area of a number of shapes. Firstly we want to consider the area of three circles. The main dimension of a circle we need to calculate the area is the radius. We put the radii of the three circles into an array, write a function to return the radius and then call it in a loop based on the number of elements in the array. Here it is in Python:
import math
#A list of the radii of three circles
circle_radii = [ 10, 20, 30 ]
#Function to calculate the area of a circle using for the formular area = pi*r^2
def calculate_area(radius):
return math.pi * (radius**2)
#Print out the area of every circle for each radii found in the circle_radii array
for circle_radius in circle_radii:
print("Shape Area:" + str(calculate_area( circle_radius )) )
But now say we want to calculate the area of four rectangles. Rectangles are defined by a height and width, not a radius. We have two dimensions that are relevant per shape and not one. We cannot put these two dimensions in the same array as the circle radii. We might make a different array to store the dimensions and make it multidimensional to account for the two dimensions.
The function to calculate the area of a circle is of no use. We need new function. We can’t call it “calculate_area” because this name is already taken, so let’s call it “calculate_area_rectangle”. That function name is quite a clear description of what it does but it’s not now less clear what exactly the preexisting calculate_area function does as there are two kinds of shape, so let’s rename that “calculate_area_circle” to improve that.
We want to print out the dimensions of rectangles but we can’t use the same loop as for the circles because we have a different number of rectangles. So we make a new loop for this shape.
import math
#A list of the radii of three circles
circle_radii = [ 10, 20, 30 ]
rectangle_dimensions = [ [ 10,10 ], [20,20], [30,30], [40,40] ]
#Calculate the area of a circle
def calculate_area_circle(radius):
return math.pi * (radius**2)
def calculate_area_rectangle(rectangle_dimension_pair):
return rectangle_dimension_pair[0] * rectangle_dimension_pair[1]
#Print out the area of every circle
for circle_radius in circle_radii:
print("Shape Area:" + str(calculate_area_circle( circle_radius )) )
#Print out the area of every rectangle
for rectangle_dimension_pair in rectangle_dimensions:
print("Shape Area:" + str(calculate_area_rectangle( rectangle_dimension_pair )) )
That works just fine, but every time we want to add a new shape, we need a new array with a new set of dimensions and we need a new loop specifically to handle that kind of shape. We also need make a new area calculating method for each shape with a unique name. Someone new to the program who wants to get the total area of all shapes, might not know what all of the kinds of shape available are and have to check documentation to make sure they have called all of the relevant area calculating functions for all of the kinds of shape that may exist. It’s not absolutely clear how this is done from just looking at the code. You also have to be clear which array goes with which method and what each dimension of the array means, which is not terribly clear either. In the rectangle array is it height followed by width or the other way around?
What we can do in OOP is group together the code for each shape with the data that it works on. This is called encapsulation. We make a kind of template that defines which parameters are relevant to each shape and which functions act upon it. The template for a shape is called a class.
Here we group together a parameter “radius” into a class (or template) called “circle” with a calculate_area() function. We group together height and width with a different calculate_area() function into a “rectangle” class. The parameters of a class, are also referred to as properties.
The class defines a generic rectangle or circle. To make a specific rectangle or circle we fill in the properties. When we make a specific shape with a defined radius or a specific height and width, we call this an object (or an instance of a class is another equivalent term).
It is conventional, although not required, to add a special procedure to a class that creates an object with the required properties. This special procedure is called a constructor. The constructor procedure accepts the key parameters that define the object. When we call the constructor we get an object back with the relevant properties filled in. You can alternatively request a blank object and fill in the properties manually. Here the constructor would accept either a radius or height/width as appropriate.
In Python, the way we make a class with a constructor to set the parameters is like this:
class Circle:
def __init__(self,radius):
self.radius = radius
def calculate_area(self):
return math.pi * (self.radius**2)
When we refer to procedures and functions in object oriented programming they are often called methods. There is no practical difference between a method and a function or procedure, it’s just the terminology that is used in OOP.
In Python the special constructor method is called __init__
and is automatically called when an object is first created from the class template. Pedants may note there is a little bit of nuance around __init__
in Python (see also __new__
) but the vast majority of the time __init__
is used as a constructor and it’s not worth confusing things at this stage by worrying about that.
The first parameter to __init__
is called self. When Python creates an object it makes an empty version of it and passes it into the constructor method as the self parameter. It is conventional in Python that self comes first in the parameter list for methods. Other programming languages do things differently and you may not get any equivalent of the self parameter at all.
In the constructor we set the radius property of self. We refer to the contents of the object using a dot. So we got passed in the empty self object, we used self.radius
to refer to the radius property within this specific object and we set it to the radius that was passed in as the 2nd parameter to the method.
Python automatically creates a radius property for the object the first time we try and use it. Many other programming languages require the properties of a class to be explicitly defined before use and would raise an error if we tried to do this, but in Python this is not required.
After the constructor we have specified a calculate_area
method. This also takes a copy of the special self parameter. We can then use this to get the radius we previously stored within the object back. So we use the radius previously stored within the object by the __init__
method as part of the area calculation formula and return it.
We can make a new Circle object with a radius of 10 like this:
my_circle_object = Circle( 10 )
So this makes an empty circle object and then calls the __init__
method automatically. That method fills in the radius property with the number “10” supplied.
And we can make an array of circles like this:
my_circle_array = [ Circle( 10 ), Circle( 20 ), Circle( 30 ) ]
We can execute the calculate_area
method of the circle object using the name of the variable holding the object and a dot. For example we can do:
area = my_circle_object.calculate_area()
area = my_circle_array[0].calculate_area()
We do not need to pass in any parameters to the calculate_area
method because it knows to use the radius we previously stored within the object.
To loop over the array of circles we can do:
for the_circle in my_circle_array:
print("Shape Area:" + str( the_circle.calculate_area() ) )
This retrieves each circle object from the array and calls the calculate_area
method for that object.
We conveniently no longer need to bother retrieving the circle radii from a separate array. Each instance of the Circle class has the relevant radius embedded directly within it.
We can also make an equivalent Rectangle class:
class Rectangle:
def __init__(self,height,width):
self.height = height
self.width = width
def calculate_area(self):
return self.height * self.width
We can similarly make Rectangles and calculate the area:
my_rectangle_array = [ Rectangle( 10,10 ), Rectangle( 20,20 ), Rectangle( 30,30 ), Rectangle( 40,40 ) ]
for the_rectangle in my_rectangle_array:
print("Shape Area:" + str( the_rectangle.calculate_area() ) )
In Python we can actually mix the rectangles and circles in one array. This enables us to get rid of the multiple arrays and only have one array containing all of the shapes together with a single loop.
my_shape_array = [ Rectangle( 10,10 ), Circle( 10 ) ]
for shape in my_shape_array:
print("Shape Area:" + str( shape.calculate_area() ) )
Sometimes programmers would prefer arrays to only contain one kind of object so they always know what to expect. This is strong typing and it can improve the performance of the language as it does not have to keep checking what kind of object is in use every time the array is accessed. Some programming languages do not even allow you to mix object types at all in an array and you can only have one sort of object in one array.
What if we were using a language with this restriction but we still wanted both kinds of object in one array? A solution is inheritance. We define a common ancestor to the Circle and Rectangle classes, which we’ll call “Shape”, and we specify that circles and rectangles are kinds of Shape. Now we can make an array of Shapes instead of a mixed array of Circle and Rectangle types. We don’t really need inheritance in Python for this specific purpose though.
Another useful thing that can be done with inheritance that is very useful in Python is we can put code that is common to both types of shapes in the Shape class and share it between Circle and Rectangle. Say we want to give shapes a colour for example which is not a concept unique to either circles or rectangles, it is a common concept shared between them. Instead of needing to repeat code that handles colours within each class, we can write the code only once in the ancestor Shape class.
In the below code, we have a Shape class which is declared as an ancestor of both Circle and Rectangle. This is achieved by placing the name of the ancestor in brackets after the name of each class e.g. Circle(Shape)
We use a concept of a setter method to set the colour of the shapes. This setter method is called “set_colour” and is defined in the Shape ancestor class. A setter method provides an interface to a property of a class. Similarly there is a getter method which retrieves the property. Rather than set the colour through a constructor parameter, we set it to a default in the constructor for each type of shape. The colour can then be subsequently changed with the setter.
import math
class Shape:
def set_colour(self, colour):
self.colour = colour
def get_colour(self):
return self.colour
class Circle(Shape):
def __init__(self,radius):
self.radius = radius
self.colour = "red"
def calculate_area(self):
return math.pi * (self.radius**2)
class Rectangle(Shape):
def __init__(self,height,width):
self.height = height
self.width = width
self.colour = "blue"
def calculate_area(self):
return self.height * self.width
Even though the set_colour and get_colour methods are not defined in Circle and Rectangle, they are inherited from Shape and are automatically available.
We can therefore do:
a = Circle(10)
print(a.get_colour())
This will print “red” which is the default colour set for circle in the init method of that class.