Object-oriented design¶
Try me¶
Object-oriented design, or Object oriented programming is a programming paradigm in which software is designed in terms of real-world objects and their relationships to one another. Recall that, as mentioned in the introduction, computer programming languages are intermediate or proxy languages between natural languages and machine code. With object-oriented programming, we create specific variables types called classes that define the properties and behaviours of concepts or objects in the scope of the problem that we are trying to solve with our program. For instance, think of a program to support the management of a course. Instead of relying exclusively on the built-in types provided by our programming language, imaging that we could have a type to model students, which would allow us to create a student by defining properties like the name and the age, just as we defined an imaginary number by defining the real part and the imaginary part, and then have methods to grade a student, or calculate the average grade of the student in an academic year. This would require the creation of a specific variable type (or class) for students, but in turn, it would allow us to code on a higher level of resemblance with the description of the problem in natural language, thus making our code easier to read and to maintain. After this introduction, let us established first the key concepts and terminology:
Class: As mentioned, the definition of an object or concept is called class, and is equivalent to the concept of a variable type. By defining custom classes, we will be able to create or instantiate variables of that type.
Object: It follows that an object is an individual instance or individual entity or a class. For instance, in the example above, you would be an instance of the class student.
Attributes: Attributes model properties of a class of objects. Examples of attributes of the class student are name, surname, email address, etc.
Methods: Methods model actions or behaviors performed over an object, possibly based on their attributes.
The following image represents the relationship between these concepts.

Benefits of Object Oriented Programming¶
The main benefits of using classes are:
Encapsulation: The process of combining attributes (data) and methods (functions) into a single object is called encapsulation. The main benefit is that data or methods might not be accessible from the context where the class is instantiated, and we can keep some data or functions private. This gives and additional level of control when accessing the object. We will then refer to:
Private access: When data or functions can only be accessed within the class, from other class methods
Public access: When data or functions can be accessed from external functions
Abstraction: We can create classes from other classes. This allows us to organise our code into different levels of abstraction, creating classes for more abstract concepts (for instance Person), and then extending these abstract classes to implement more concrete concepts (for instance Customer, or Employee). This allows us to hide implementation details when using a parent class.
Inheritance: Another benefit of creating classes from other classes is inheritance. We can implement common functionality in the parent class (Person) and reuse this functionality in the children classes (for instance, a send_email() method to send an email to a person, regardless if it´s a customer or an employee).This allows us to reduce redundant code, by implementing common methods and attributes in parent classes.
Polymorphism: Another benefit that arises from the fact that we can create classes from other classes is that we can use the same method name to implement different functionalities in each child class. For instance, following the example above, both Customer and Employee classes could have an attribute called
welcome_messagewith a welcome message text, but, although the concept is the same, the text can be different for customers than for employees. This allows to make our code more consistent and easier to read, by having common names for common concepts that can have different implementations.
Definition of classes¶
Let us now see how to create classes. Basically, what we need to do is to use the keyword class followed by the name of the class that we want to create, and then, within the class definition, use the def keyword to add methods to our class. Let us see an example and then explain the syntax.
[5]:
class MyClass:
def __init__(self, attr1, attr2): #Constructor definition
self.attribute1 = attr1
self.attribute2 = attr2
def show(self): # method
print(self.attribute1)
print(self.attribute2)
# Instantiation
my_object = MyClass("value1", "value2")
# Use
my_object.show()
value1
value2
The first method in the example is the constructor, the function that will be automatically called to create an instance of the class. The constructor name is always __init__. The double underscore is used to note that this is an internal method, not meant to be called on an instance, and the init is clearly a shorthand for initialization, since this is the method that will be used to initialise a new instance of the class. Also, note that the first argument of any method in the class,
including the constructor, is self, which represents the instance of the class: The variable in the context where the class has been instantiated that implements the class (my_object in the example). In the constructor, we use the variable self to assign the values provided in the main context to the properties of the class.
Let´s deepen into these concepts with another example. Let us implement a class for Polygon objects. For now, our Polygons have only one property: The number of sides. We will also add a method print_number_of_sides to show the number of sides of the polygon:
[6]:
class Polygon:
def __init__(self, sides):
self.sides = sides #the constructor assigns the value "sides" passed in the constructor to the property sides of the instance (self.sides)
def print_number_of_sides(self): #This is a method of the class
print(self.sides)
To create an instance of our the polygon, we use the name of the class (Polygon), passing the arguments of the initialisation method. Once we have created the instance and assigned it to a variable, we can access its methods and properties:
[7]:
poly = Polygon(7)
# Show the number of sides calling the method print_number_of_sides()
poly.print_number_of_sides()
# Show the number of sides accessing the attribute sides
print(poly.sides)
7
7
Abstraction¶
We can create a subclass or a child class that inherits all methods and properties of its parent class, using abstraction. To extend a class, we need to pass the parent class between parenthesis following the class name in the class creation statement. We can access the parent class instance using the keyword super(). For instance:
[8]:
class Square(Polygon): #The parent is defined as a parameter in the class definition.
def __init__(self, color):
super().__init__(4) #First we instantiate the parent class using super()
self.color = color # We add a second property
def change_color(self, new_color): # We define a new method, change color
self.color = new_color
def print_color(self): # We define a new method, print color
print(self.color)
Note that we have created a class Square from the parent class Polygon by passing it between parenthesis (as if it was a parameter) in the class definition. Note also that, as a concept, Polygon is more abstract than a Square, as all squares are polygons. All methods and attributes from the parent class will be available and can be extended in the children class to implement more concrete concepts.
In the constructor, we need to initialise the instance of the parent class through its constructor using super().__init__(). Recall that the constructor of the class Polygon had an attribute which was the number of sides. Since a square has 4 sides, we use 4 as the argument in the init method. Finally, we extend our class with another attribute (color) and add two methods to change the color and to print the color. Let us see our brand new class in action:
[9]:
s = Square("blue")
s.print_number_of_sides()
s.print_color()
s.change_color("red")
s.print_color()
4
blue
red
Note that we do not need to specify the number of sides when we create a square, since the number of sides is defined internally in the square class (hiding this implementation detail from the user). We can still access the methods and attributes of the parent class, for instance the print_number_of_sides() method.
Inheritance¶
Inheritance allows us to reuse the code from parent and share common code. Following the example above, we can create another class called Pentagon:
[10]:
class Pentagon(Polygon):
def __init__(self):
super().__init__(5)
And still reuse the number sides features implemented in the parent class Polygon.
[11]:
pent = Pentagon()
pent.print_number_of_sides()
print(pent.sides)
5
5
Polymorphism¶
Objects with a common base can implement the same methods doing different things. For instance, following the example above, we can draw different polygons in different ways depending on their shape:
[12]:
class Triangle(Polygon):
def __init__(self):
super().__init__(3)
def draw(self):
print("This is a triangle:")
print(" ^ ")
print(" / \\")
print("/___\\")
class Rectangle(Polygon):
def __init__(self):
super().__init__(4)
def draw(self):
print("This is a rectangle:")
print("+----+")
print("| |")
print("| |")
print("+----+")
t = Triangle()
r = Rectangle()
t.draw()
r.draw()
This is a triangle:
^
/ \
/___\
This is a rectangle:
+----+
| |
| |
+----+
Notice that, since the concept of drawing is the same, we use the same method name regardless of the type of polygon we have created. Note also that we do not need to know the concrete type of polygon to draw it, we do not need to know the specific class as long as we know that it is a polygon, so, for instance, we could do something like:
[13]:
# We can can call draw method without knowing which kind of polygon is
polygons = [t, r]
for p in polygons:
p.draw()
This is a triangle:
^
/ \
/___\
This is a rectangle:
+----+
| |
| |
+----+
Encapsulation¶
Finally, we left encapsulation for last because it is not implement in-depth in Python. To define a private function or attribute that can only be accessible within the class, you just need to use the double underscore notation, just as we explained for the __init__ method used as constructor. For instance:
[16]:
class MyEncoder:
def __init__(self, attr1, attr2):
self.__private_atribute__ = 2 #This is a private attribute can only be accessed internally
self.attr1 = attr1
self.attr2 = attr2
def __internal_formula__(self):
return (self.attr1 + self.attr2)**self.__private_atribute__
def encode_number(self, number):
return number * self.__internal_formula__()
my_encoder = MyEncoder(2, 3)
encoded_number = my_encoder.encode_number(5)
print(encoded_number)
125
In the example, the encoder class uses an internal attribute and an internal formula to encode a number. By using the double underscore notation, these attributes and methods are noted as private, however, they can still be accessed in the main context, because the double underscore is just a convention. Be aware of this when you use external libraries, because this notation is telling you that the developer did not mean those methods and attributes to be used externally and you might get unexpected results!