Introduction
Object-Oriented Programming (OOP) is a method of structuring a program by clustering common properties and behavior into individual objects. OOP supports code reusability, abstraction, and encapsulation. OOP is based on the concept of objects that contains code and data. OOP languages are diverse, but the most popular is class-based, where objects are instances of the 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 a user-defined prototype for an object that defines an attribute that characterizes the object of a class. Class cluster the data and methods. In the above example, “Student” is the name of a class.
- Object: A unique instance of data structure that is defined by its class. The object comprises data and methods. In the above example, s1 is an object of class “Student” and is created using the name of the class.
- Method: Methods are nothing but functions that are defined inside a class. In the above example __init__ and show() are two methods.
- Class Variable: class variables are those variables that are defined inside a class but outside any of the methods of the class. In the above example, College is the class variable.
- Instance variable: Instance variables are those variables that are defined inside any method. In the above example, name, id, address, and color are instance variables.
- __init__(self): This is a special kind of method that is known as a constructor or initializer. Here, Python uses self-in methods to represent instances of that class. With this self keyword, an object can access the data and methods of the 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 a tuple, list, and dictionary format.
- __dict__ is used to get the elements of the 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 such 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 every time 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 the user to transfer all the characteristics of the base class to the derived class. The object of derived class can also access the data and methods of its 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 a simple example of inheritance, where we derived the Employee class from the Student class. When the object of the derived class is made, it calls the derived class constructor. Since the Employee class is derived from the Student class, the object of the Employee class can access the method of the Student class i.e show() method.
A child or derived class can have more than one parent class, which is known as multiple inheritances.
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 inheritances 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 the method and data of parent classes. In the above example, MRO for Employee shows, first of all, the method of the derived class will be executed and then class Student and then Manager because it follows the order in which class is derived from the parent class.
Multiple classes can be derived from the 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 the same name and same parameters is not allowed. We can modify the method such that it can show different reactions according to different numbers and types of parameters which are 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 its parameters. First of all, no parameters were passed so it shows nothing to show. While passing name and id at the same time it shows both the name and id. Also when the only name is passed and only the id is passed the same function was able to handle all the activities. So, this is the advantage of method overloading.
Method overriding
Method overriding is the ability of programming language that allows the method with the same name and same parameters in both parent and child class. If both parent and child class have same method with same parameters then the 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 the same method named as to 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 any task. + Operator in python performs integer 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'
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 the < operator so that it can compare two instances of the class. We use the __lt__() method which is a 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 to the individual instances of a class. When we execute s1 < s2, this statement is equivalent to s1.__lt__(s2). An instance of a class is passed as an 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 a 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 actually need to change them, we should use the 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'
The Employee class is the child class of the Student class. By default all the method and instance variables must be inherited from the child class. But we got an attribute error that says the derived class doesn’t have the __name variable because it is a private variable of the base class so it hides that variable in the derived class.
Use of super() in OOP
super() returns the temporary object of super class that allows the user to access the method of the 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 the derived class while super() is still the same and still we got the correct output. This means we can change the name of the 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, and 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 the base class in the derived class without using the name of a derived class. Classes allow us to bind the data and method together.
Happy Learning 🙂