Introduction

Object Oriented Programming (OOP) is a method of structuring a program by clustering common properties and behavior into individual objects. OOP supports code resusability, abstraction and encapsulation. OOP is based on concept of objects that contains code and data. OOP languages are diverse, but most popular is class based, where objects are instances of class and also determine their types.

Let's take an example and understand the terms 

class Student:
	College = "HCOE"
	
	def __init__(self, name, id, address):
		self.name = name
		self.id = id
		self.address = address
		self.dress_color = "white"
		
	def show(self):
		print("Name: ", self.name)
		print("Id: ", self.id)
		print("Address: ", self.address)
			
s1 = Student("Shiv", 34, "Rupandehi")

s2 = Student("Rajesh", 35, "Kapilvastu")

print("First student: \n")
s1.show()

print("\nSecond student: \n")
s2.show()

Output

First student:

Name:  Shiv
Id:  34
Address:  Rupandehi

Second student:

Name:  Rajesh
Id:  35
Address:  Kapilvastu

 

  • Class : Class is user-defined prototype for object that defines attribute that characterizes the object of a class. Class cluster the data and methods. In above example "Student" is name of a class.
  • Object : A unique instance of data structures that's defined by it's class. Object comprises of data and methods. In above example s1 is object of class "Student" and is created using name of class.
  • Method : Methods are nothing but function that are defined inside a class. In above example __init__ and show() are two methods.
  • Class Variable : class variable are those variables that are defined inside a class but outside any of the methods of class. In above example, College is the class variable.
  • Instance variable : Instance variable are those variables that are defined inside any methods. In above example, name, id , address, color are instance variables.
  • __init__(self): This is a special kind of method that is known as constructor or initializer. Here, Python uses self in methods to represent instance of that class. With this self keyword object can access the data and methods of class.
  • Instance : Instance is the individual object of a class. 

Let's look at some of the built-in methods

class Student:
	def __init__(self,, name, id, address):
		self.name = name
		self.id = id
		self.address = address
		
	def __str__(self):
		return self.name + " lives in " +self.address + " and has id no. " + str(self.id)
		
	def __repr__(self):
		return [self.name, self.id, self.address]
		
s1 = Student("Shiv", 34, "Hong Kong")
s2 = Student("Ram", 45, "Jajarkot")

print(s1)
print(s2)
print("\n")

print(s1.__repr__())
print(s2.__repr__())

print("\n")
print(s1.__dict__)
print(s2.__dict__)

Output

Shiv lives in Hong Kong and has id no. 34
Ram lives in Jajarkot and has id no. 45


['Shiv', 34, 'Hong Kong']
['Ram', 45, 'Jajarkot']


{'name': 'Shiv', 'id': 34, 'address': 'Hong Kong'}
{'name': 'Ram', 'id': 45, 'address': 'Jajarkot'}

 

  • __str__(self): This method returns the output in string format.
  • __repr__(self): This method doesn't return the output in string format . It returns in tuple, list and dictionary format.
  • __dict__ is used to get the elements of class into dictionary format.

Let's say we want to set items and get items inside a class. We can do this job using methods as

class Student:
	def __init__(self):
		self.info= {}
		
	def setitems(self, key, value):
		self.info[key] = value
		
	def getitems(self, key):
		return self.info[key]
		
s1 = Student()

#setting items
s1.setitems("Name", "Rajesh")
s1.setitems("Address", "Kapilvastu")

#getting items
print(s1.getitems("Name"))
print(s1.getitems("Address"))

Output

Rajesh
Kapilvastu

We got what we wanted to do. But we've  to call those two functions everytime when we want to set and get items which can be a little bit tedious work to do. Python has __setitems__() and __getitems__() to perform the same task as above. Here's how it can be done

class Student:
	def __init__(self):
		self.info= {}
		
	def __setitem__(self, key, value):
		self.info[key] = value
		
	def __getitem__(self, key):
		return self.info[key]
		
		
s1 = Student()

#setting items
s1["Name"] = "Rakesh"
s1["Id"] = 34
s1["Address"] = "Taplejung"

#getting items
print("Name : ", s1["Name"])
print("Id : ", s1["Id"])
print("Address : ", s1["Address"])

Output

Name :  Rakesh
Id :  34
Address :  Taplejung

In this way we can use __setitem__() and __getitem__().

 

Inheritance

Inheritance allows user to transfer all the characteristics of base class to the derived class. Object of derived class can also access the data and methods of it's parent class

class Student:
	college = "HCOE"
	
	def __init__(self):
		print("Base class constructor.")

	def show(self):
		print("i'm in Base class.")
		
class Employee(Student):
	def __init__(self):
		print("Derived class constructor.")
		
	def display(self):
		print("i'm in derived class.")
	
#object of derived class - Employee
e1 = Employee()

#accessing class variable
print(e1.college)

#accessing Base class method from derived class
e1.show()

#accessing own method
e1.display()

Output

Derived class constructor.
HCOE
i'm in Base class.
i'm in derived class.

This is simple example of inheritance, where we derived Employee class from Student class. When the object of derived class is made, it calls the derived class constructor. Since, Employee class is derived from Student class, object of Employee class can access method of Student class i.e show() method.

Child or derived class can have more than one parent class, which is knows as multiple inheritance.

class Student:
	def __init__(self):
		print("Base class constructor - Student.")

	def show(self):
		print("i'm in Base class - Student.")
		
class Manager:
	def __init__(self):
		print("Base class constructor - Manager.")

	def flash(self):
		print("i'm in Base class - Manager.")
		
class Employee(Student, Manager):
	def __init__(self):
		print("Derived class constructor.")
		
	def display(self):
		print("i'm in derived class.")
	
#object of derived class - Employee
e1 = Employee()

#accessing Base class method from derived class
e1.show()


#accessing Base class method from derived class
e1.flash()

#accessing own method
e1.display()
print("\n")

#method resolution order for Derived class
print(Employee.mro())

Output

Derived class constructor.
i'm in Base class - Student.
i'm in Base class - Manager.
i'm in derived class.


[<class '__main__.Employee'>, <class '__main__.Student'>, <class '__main__.Manager'>, <class 'object'>]

This is how we can do multiple inheritance in python. Method Resolution Order (MRO) for derived class shows that first of all, python looks for the methods and data of the derived class and then for method and data of parent classes. In above example, MRO for Employee shows, first of all, method  of derived class will be executed and then class Student and then Manager because it follows the order in which class are derived from parent class.

Multiple class can be derived from single base class. Here's how it can be done

class Student:
	def __init__(self):
		print("Base class constructor.")

	def show(self):
		print("i'm in Base class.")
		
class Manager(Student):
	def __init__(self):
		print("Derived class constructor - Manager.")

	def display(self):
		print("i'm in derived class - Manager.")
		
class Employee(Student):
	def __init__(self):
		print("Derived class constructor - Student.")
		
	def display(self):
		print("i'm in derived class - Student.")
	
#object of derived class
m1 = Manager()
e1 = Employee()
print("\n")

#accessing Base class method from derived class
e1.show()
m1.show()
print("\n")

#accessing own method
e1.display()
m1.display()
print("\n")

#displaying MRO for both derived class
print(Employee.mro())
print("\n")
print(Manager.mro())

Output

Derived class constructor - Manager.
Derived class constructor - Student.


i'm in Base class.
i'm in Base class.


i'm in derived class - Student.
i'm in derived class - Manager.


[<class '__main__.Employee'>, <class '__main__.Student'>, <class 'object'>]


[<class '__main__.Manager'>, <class '__main__.Student'>, <class 'object'>]

 

Method overloading

Two methods with same name and same parameters ia not alloweded. We can modify the method such that it can show different reaction according to different number and types of parameter which is called method overloading.

class Student:
	def show(self,name = None, id = None):
		if name !=None and id == None:
			print("Name : ", name)
			
		elif name == None and id != None:
			print("Id : ", id)
			
		elif name is None and id is None:
			print("Nothing to show")
			
		else:
			print({"Name":name, "Id":id})


s1 = Student()
#passing nothing
s1.show()

#passing only name
s1.show(name = "Ram")

#passing only id
s1.show(id = 45)

#passing both name and id
s1.show("ram", 45)

Output

Nothing to show
Name :  Ram
Id :  45
{'Name': 'ram', 'Id': 45}

Here, class student has a method called show(). This method is overloaded so it can perform multiple functions based on it's parameters. First of all, no parameters were passed so it shows nothing to show. While passing name and id at same time it shows both the name and id. Also when only name is passsed and only id is passed same function was able to handle all the activities. So, thia is advantage of method overloading.

 

Method overriding

Method overriding is the ability of programming language that allows the method with same name and same parameters in both parent and child class. If both parent and child class have same method with same parameters then method of child class is said to override the method of parent class.

class Student:
	def show(self):
		print("This is base class - Student")
		
class Employee(Student):
	def show(self):
		print("This is derived class - Employee")
		

e1 = Employee()
e1.show()

Output

This is derived class - Employee

Here, the base class and derived class have same method named as show(). The derived class method overrides the method of base class.

 

Operator overloading

Operator overloading is the technique in which an operator is made to perform many task. + Operator in python performs interger addition and string concatenation, which is possible because of operator overloading.

class Student:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	def show(self):
		print(self.name)
		print(self.address)
			
s1 = Student("Rajesh", "Kapilvastu")
s2 = Student("Shiv", "Rupandehi")

print(s1 < s2)

Output

Traceback (most recent call last):
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 31, in <module>
    start(fakepyfile,mainpyfile)
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 30, in start
    exec(open(mainpyfile).read(),  __main__.__dict__)
  File "<string>", line 13, in <module>
TypeError: '<' not supported between instances of 'Student' and 'Student'

[Program finished]

Here, we got the type error as < operator cannot compare two instances of class directly. If we want to compare two instances of class we need to overload the operator.

 

class Student:
	def __init__(self, name, address):
		self.name = name
		self.address = address
	
	def __lt__(self, other):
		l1 = len(self.name) + len(self.address)
		l2 = len(other.name) + len(other.address)
		return l1 < l2
		
	def show(self):
		print(self.name)
		print(self.address)
			
s1 = Student("Rajesh", "Kapilvastu")
s2 = Student("Shiv", "Rupandehi")

print(s1 < s2)
print(s1 > s2)

Output

False
True

We overloaded < operator so that it can compare two instances of class. We use __lt__() method which is built-in method to overload the operator. Two parameter are passed in this method, one is 'self' and another is 'other'. They are used to point the individual instances of class. When we execute s1 < s2, this statement is equivalent to s1.__lt__(s2). A instance of class is passed as argument.

 

Encapsulation

With the help of OOP, Python provides data encapsulation by which we can restrict some data and methods from being accessed. For this single underscore (_) or double underscore(__) is used as prefix.

class Student:
	def __init__(self):
		self.__name = "rovman"
		self._address = "Nepaljung"
		
	def show(self):
		print(self.__name)
		print(self._address)
			
		
s1 = Student()
print("Before updating name and address: ")
s1.show()

print("\n")

#update values
s1.__name = "carlos"
s1._addrees = "Dang"
print("after updating name and address")
s1.show()

Output

Before updating name and address:
rovman
Nepaljung


after updating name and address
rovman
Nepaljung

We can got same output instead of updating those two instance variables because python treats them as private variables. If we want to actually need to change them, we should use setter function.

Let's take another example of encapsulation

class Student:
	def __init__(self):
		self.__name = "rovman"
		self._address = "Nepaljung"
		
	def show(self):
		print(self.__name)
		print(self._address)
		
class Employee(Student):
	pass
			
		
e1 = Employee()
print(e1.__name)

Output

Traceback (most recent call last):
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 31, in <module>
    start(fakepyfile,mainpyfile)
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 30, in start
    exec(open(mainpyfile).read(),  __main__.__dict__)
  File "<string>", line 15, in <module>
AttributeError: 'Employee' object has no attribute '__name'

Employee class is child class of Student class. By default all the method and instance variables must be inherited to child class. But we got attribute error which says derived class doesn't have __name variable because it is private variable of base class so it hides that variable in derived class.

 

Use of super() in OOP

super() returns the temporary object of super class that allows user to access the method of base class.

class Student:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	def show(self):
		print(self.name)
		print(self.address)
		
class Employee(Student):
	def __init__(self):
		super().__init__("Rajesh", "Kapilvastu")
			
		
e1 = Employee()
e1.show()

Output

Rajesh
Kapilvastu

Here, super() is used to call the base class constructor to initialize the name and address variable. We assigned the values by calling super().__init__(self, name, address) instead of e1.__init__(self, name, address). Here the important thing is that we assigned the variables using base class constructor without using name of derived class. With this we can change the name of derived class at any time. Here's how

class Student:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	def show(self):
		print(self.name)
		print(self.address)
		
class E(Student):
	def __init__(self):
		#this remains same
		super().__init__("Rajesh", "Kapilvastu")
			
		
e1 = E()
e1.show()

Output

Rajesh
Kapilvastu

We change the name of derived class while super() is still the same and still we got correct output. This what means we can change the name of derived class at any time.

class Student:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	def show(self):
		print(self.name)
		print(self.address)
		
class E(Student):
	def __init__(self):
		#this remains same
		super().__init__("Rajesh", "Kapilvastu")
			
	def display(self):
		super().show()
		
e1 = E()
e1.display()

Output

Rajesh
Kapilvastu

We can use super() to access the method of base class also.

 

Conclusion

OOP in Python provides data encapsulation, inheritance, polymorphism which is achieved by operator overloading and method overloading. super() built-in function return the product object of base class. Using super(), we can access the data and methods of base class in derived class without using name of derived class. Classes allows us to bind the data and method together.

Happy Learning :-)

References

https://www.tutorialspoint.com/python/