Implementation of OOP concepts in Python

Classes and Objects are fundamental parts of Object Oriented Programming (OOP). Python is an OOP language and almost everything (including strings, numbers, lists) are seen as objects. In OOP, an object has also "fuctions" and they are called "methods". The object is an instance of a class: the class is a template that describes all the properties and methods while the object is a specific entity which has the class structure but filled with data. Details on Classes and Objects in Python can be found by clicking the button below.

This section discusses on how Python implements the main concepts that are the fundation of Object Oriented Programming: Encapsulation, Inheritance, Polymorphism and Abstraction.

Encapsulation

Let's begin with Encapsulation which refers to the bundling of data, and the methods that operate on that data, into a single unit. Also, there are typically some mechanism that prevent the access to specific data or functions of the objects (the user should not necessarily know on how everything works, some implementative details are not exposed to the user) and these data and functions are typically used within specific functions or data which are exposed to the user.

In Python there is the convention of not accessing variables and methods which begin with an underscore "_": Python allows to access these entities but a programmer should know that they are not meant to be exposed to the users, they are "protected". A more restrictive version of this is the double underscore "__": when trying to access a variable or function with "__" at the beginning Python throws an error because does not find it. These entities are "private". It must be said that there are ways to still access these entities but the point is that the error is a way of explicitly telling the programmer that the entities are not meant to be accessed externally.

class Square:
	def __init__(self,side):  #Square class with side variable
		self._side=side       # Protected variable _side
		
	def _calculate_perimeter(self):    # Method is protected
		return self._side*4

	def print_perimeter(self):    # Method is public
		print("The perimeter is",self._calculate_perimeter())
		
s=Square(5)
print(s._side)   				 # Prints 5, is defined as private but can be accessed
print(s._calculate_perimeter())	 # Prints 20, again private but accessible
s.print_perimeter()				 # Public, it prints "The perimeter is 20"

class Secure_Square:		  # Same class but with double underscore
	def __init__(self,side):  #Square class with side variable
		self.__side=side       # Private variable __side
		
	def __calculate_perimeter(self):    # Method is private
		return self.__side*4

	def print_perimeter(self):    # Method is public
		print("The perimeter is",self.__calculate_perimeter())
		
s=Secure_Square(5)
print(s._side)   				 # Throws an error
print(s._calculate_perimeter())	 # Throws an error
s.print_perimeter()				 # Public, it prints "The perimeter is 20"

The external public functions used let the user interact with the class are called setters and getters: they are able to modify values that are private/protected or access private/protected values. The example above shows a getter (it gets the private variable __side) while the example below shows both.

class Store_Number:
	def __init__(self,number):
		self.__number=number     # Private __number
	
	def get_number(self):		 # Public getter
		return self.__number
		
	def set_number(self,number): # Public setter
		self.__number=number

n=Store_Number(10)
print(n.__number)       # Throws an error
print(n.get_number())  	# Prints 10
n.set_number(15)
print(n.get_number())  	# Prints 15
Inheritance

The second concept discussed is Inheritance which allows a "child" class to inherit the variables, proprieties and methods of a "parent" class. This is done by simply creating the "child" class with the name of the "parent" class within round brackets. Inheritance is useful to avoid rewriting similar functionalities and efficiently write variations of a particular code. Every method re-written in the child class is overridden, otherwise the parent method is used. In case a child method needs to call a parent method it can simply use the name of the parent class, the dot and the name of the method. Another way of doing this is by using the function super() which automatically selects the parent class without the need of writing a specific name.

#Parent class
class Shape:
	def __init__(self,base=0,height=0):
		self.base=base 
		self.height=height 		

	def print_message(self):   
		print("This is a 2D shape!")

	def print_area(self):   
		print("Shape not defined!")

class Square(Shape):    # Square is child of Shape
	def __init__(self,side=0):   # Override the constructor
		print("This is a Square")
		self.side=side 		
		
	def print_area(self):	#Override print_area method
		print("Square area is:",self.side**2)
		
	def print_perimeter(self):	#Add new method not included in parent class
		print("Square perimeter is:",self.side*4)

class Rectangle(Shape):    # Rectangle is child of Shape
	def __init__(self,base=0,height=0):   # Override the constructor
		print("This is a Rectangle")      # The only difference we want is to add this sentence
		super().__init__(base,height)	  # Instead of rewriting the two variables we can use super()		
		
	def print_area(self):	#Override print_area method
		print("Rectangle area is:",self.base*self.height)
		
shape=Shape() # No need arguments because the defaults are 0
shape.print_area()       # Prints "Shape not defined!"
shape.print_message()    # Prints "This is a 2D shape!"
square=Square(5)         # Prints "This is a Square"
square.print_area()      # Prints "Square area is 25"
square.print_perimeter() # Prints "Square perimeter is 20"
square.print_message()    # Prints again "This is a 2D shape!". This is inherited and not changed.
rectangle=Rectangle(4,3) # Prints "This is a Rectangle"
rectangle.print_area()   # Prints "Rectangle area is 12"
rectangle.print_message() # Prints again "This is a 2D shape!". This is inherited and not changed.

In the example above we have seen that a single parent class can be inherited by more than one child classes. It is true also the opposite, a single child class can inherit from more than one parent class: this can be done by including more than one parent class within round brackets.

class Parent1:	#First parent class
	def Parent1_message(self):   
		print("Inherited from Parent1")

class Parent2:	#Second parent class
	def Parent2_message(self):   
		print("Inherited from Parent2")

class Child(Parent1,Parent2):	# This class inherits from both classes
	def Child_message(self):   
		print("This method is not inherited!")

c=Child()
c.Parent1_message() #Prints "Inherited from Parent1"
c.Parent2_message() #Prints "Inherited from Parent2"	
c.Child_message()   #Prints "This method is not inherited!"	

It is also possible to inherit multiple levels of classes: a class that inherits from another class which inherits from another class and so on.

class Class1:
	def __init__(self,value1=0): # Initialize value1 in Class1
		self.class1_var=value1
	
	def Print_value1(self): 	# Print value1
		print(self.class1_var)

class Class2(Class1):   # Inherits from Class1
	def __init__(self,value1=0,value2=0):	#Initialize value2 in Class2
		self.class2_var=value2
		Class1.__init__(self,value1)     	#Call constructor in Class1 to initialize value1
		
	def Print_value2(self): 				# Print value2
		print(self.class2_var)
		
class Class3(Class2):  # Inherits from Class2
	def __init__(self,value1=0,value2=0,value3=0):	#Initialize value2 in Class2
		self.class3_var=value3
		Class2.__init__(self,value1,value2)     	#Call constructor in Class2 to initialize value2 which then calls constructor in Class1 for value1
		
	def Print_value3(self): 				# Print value3
		print(self.class3_var)
		
c3=Class3(5,8,20)
c3.Print_value3()  # Prints 20
c3.Print_value2()  # Prints 8
c3.Print_value1()  # Prints 5
Polymorphism

Polymorphism is another one of the core concepts of object-oriented programming and it is related to the situations where something occurs in different forms: it describes the possibility to access objects of different types through the same interface. This is important because in this way it is possible to re-use some code for several circumstances. In Python some in-built functions show already polymorphism, for example the function len() can be used for lists or tuples or strings and in all these cases count the elements but these are different data types.

print(len("Hello"))	 # Prints 5, because it counts characters
print(len((1,5,8)))	 # Prints 3, it counts elements of the tuple
print(len(["hi",5,10,4.5])) # Prints 4, it counts elements of the list

Another typical example of polymorphism is the ability in Python to assign the same variable to different classes and, if they have the same method names, they can be directly called by the variable.

class Square:
	def message(self):   
		print("This is a square!")

class Triangle:
	def message(self):   
		print("This is a triangle!")

class Circle:
	def message(self):   
		print("This is a circle!")
		
s=Square()
t=Triangle()
c=Circle()
shapes_list=[s,t,c]  # List of these objects
# The for loop use a single variable and a single method call
# The output of the method depends on the list of objects
for i in shapes_list:
	i.message()      # This prints all three messages	

The same can be done also through functions: a function expect an object with a specific method without concerning which is the object as long as the method exists.

class Square:
	def message(self):   
		print("This is a square!")

class Triangle:
	def message(self):   
		print("This is a triangle!")

class Circle:
	def message(self):   
		print("This is a circle!")
		
s=Square()
t=Triangle()
c=Circle()

def Print_Message(shape):  # The input is a "shape"
	shape.message()		   # This object needs to have a method "message()" 
	
Print_Message(s)    # Prints "This is a square!"
Print_Message(t)    # Prints "This is a triangle!"
Print_Message(c)    # Prints "This is a circle!"

Another example of polymorphism is called "Method Overriding". This is related to inheritance and allows to change a method of the parent class within a child class in order to have a specific behavior.

class Power:	# Class that raises number to the power of 1
	def power(self,value):
		return value**1
		
class Power2(Power): 	# Class that raises to the power of 2
	def power(self,value):  # Overrides Power method
		return value**2
		
class Power3(Power): 	# Class that raises to the power of 3
	def power(self,value):  # Overrides Power method
		return value**3

p1=Power()
p2=Power2()
p3=Power3()

print(p1.power(4))   # Prints 4
print(p2.power(4))   # Same method, prints 16 (4^2)
print(p3.power(4))   # Same method, prints 64 (4^3)		
Abstraction

Abstraction is the last core concept of OOP. This has to do, as the word says, with handling complexity and hiding unnecessary information: abstract the class to its principal features that need to be exposed to the user. In this way the user does not have to know how things are working inside but simply understanding which is the main structure and which methods to call. In Python this is done by importing the ABC library (ABC means "Abstract Base Classes") and the abstract class, which defines the main structure, needs to inherits ABC. Also, the abstract class only defines the methods names and arguments but not their content given that the content will be defined by the child classes that will inherit the abstract class: for this reason, the abstract class cannot be instantiated into an object.

#Import ABC and abstractmethod
from abc import ABC, abstractmethod

#This is the abstract class
class Operation(ABC):
	@abstractmethod                    # This is a "decorator" which tells that the method is abstract
	def Calculate(self,value1,value2): # Abstract method which should calculate something
		pass

class Add(Operation):
	def Calculate(self,value1,value2):  # Adds value1 and value2
		print(value1+value2)
		
class Subtract(Operation):
	def Calculate(self,value1,value2):  # Subtracts value2 from value1
		print(value1-value2)
		
class Multiply(Operation):
	def Calculate(self,value1,value2):  # Multiplies value1 and value2
		print(value1*value2)
		
class Divide(Operation):
	def Calculate(self,value1,value2):  # Divides value1 and value2
		print(value1/value2)

o=Operation()  # This throws an error because it is an abstract class

a=Add()
s=Subtract()
m=Multiply()
d=Divide()
#All these objects follow the same structure (there is an operation and needs to be calculated)
#The user just needs to know that by "Calculating" the operation of the object is performed.
a.Calculate(5,4)   # Prints 9
s.Calculate(5,4)   # Prints 1
m.Calculate(5,4)   # Prints 20
d.Calculate(5,4)   # Prints 1.25

This section explained how OOP concepts are implemented within Python. Use the console below and try some of the operations described.