Category: Object Oriented Programming

https://zain.sweetdishy.com/wp-content/uploads/2025/12/python-3.png

  • Python – Abstraction

    Abstraction is one of the important principles of object-oriented programming. It refers to a programming approach by which only the relevant data about an object is exposed, hiding all the other details. This approach helps in reducing the complexity and increasing the efficiency of application development.

    Types of Python Abstraction

    There are two types of abstraction. One is data abstraction, wherein the original data entity is hidden via a data structure that can internally work through the hidden data entities. Another type is called process abstraction. It refers to hiding the underlying implementation details of a process.

    Python Abstract Class

    In object-oriented programming terminology, a class is said to be an abstract class if it cannot be instantiated, that is you can have an object of an abstract class. You can however use it as a base or parent class for constructing other classes.

    Create an Abstract Class

    To create an abstract class in Python, it must inherit the ABC class that is defined in the ABC module. This module is available in Python’s standard library. Moreover, the class must have at least one abstract method. Again, an abstract method is the one which cannot be called but can be overridden. You need to decorate it with @abstractmethod decorator.

    Example: Create an Absctract Class

    from abc import ABC, abstractmethod
    classdemo(ABC):@abstractmethoddefmethod1(self):print("abstract method")returndefmethod2(self):print("concrete method")

    The demo class inherits ABC class. There is a method1() which is an abstract method. Note that the class may have other non-abstract (concrete) methods.

    If you try to declare an object of demo class, Python raises TypeError −

       obj = demo()
             ^^^^^^
    TypeError: Can't instantiate abstract class demo with abstract method method1
    

    The demo class here may be used as parent for another class. However, the child class must override the abstract method in parent class. If not, Python throws this error −

    TypeError: Can't instantiate abstract class concreteclass with abstract method method1
    

    Abstract Method Overriding

    Hence, the child class with the abstract method overridden is given in the following example −

    Example

    from abc import ABC, abstractmethod
    classdemoclass(ABC):@abstractmethoddefmethod1(self):print("abstract method")returndefmethod2(self):print("concrete method")classconcreteclass(democlass):defmethod1(self):super().method1()return
          
    obj = concreteclass()
    obj.method1()
    obj.method2()

    Output

    When you execute this code, it will produce the following output −

    abstract method
    concrete method
  • Python – Dynamic Typing

    One of the standout features of Python language is that it is a dynamically typed language. The compiler-based languages C/C++, Java, etc. are statically typed. Let us try to understand the difference between static typing and dynamic typing.

    In a statically typed language, each variable and its data type must be declared before assigning it a value. Any other type of value is not acceptable to the compiler, and it raises a compile-time error.

    Let us take the following snippet of a Java program −

    public classMyClass{
       public static void main(String args[]){int var;
          var="Hello";
          
          System.out.println("Value of var = "+ var);}}

    Here, var is declared as an integer variable. When we try to assign it a string value, the compiler gives the following error message −

    /MyClass.java:4: error: incompatible types: String cannot be converted to int
       x="Hello";
         ^
    1 error
    

    Why Python is Called Dynamically Typed?

    variable in Python is only a label, or reference to the object stored in the memory, and not a named memory location. Hence, the prior declaration of type is not needed. Because it’s just a label, it can be put on another object, which may be of any type.

    In Java, the type of the variable decides what it can store and what not. In Python, it is the other way around. Here, the type of data (i.e. object) decides the type of the variable. To begin with, let us store a string in the variable in check its type.

    >>> var="Hello">>>print("id of var is ",id(var))id of var is2822590451184>>>print("type of var is ",type(var))type of var is<class'str'>

    So, var is of string type. However, it is not permanently bound. It’s just a label; and can be assigned to any other type of object, say a float, which will be stored with a different id() −

    >>> var=25.50>>>print("id of var is ",id(var))id of var is2822589562256>>>print("type of var is ",type(var))type of var is<class'float'>

    or a tuple. The var label now sits on a different object.

    >>> var=(10,20,30)>>>print("id of var is ",id(var))id of var is2822592028992>>>print("type of var is ",type(var))type of var is<class'tuple'>

    We can see that the type of var changes every time it refers to a new object. That’s why Python is a dynamically typed language.

    Dynamic typing feature of Python makes it flexible compared to C/C++ and Java. However, it is prone to runtime errors, so the programmer has to be careful.

  • Python – Dynamic Binding

    In object-oriented programming, the concept of dynamic binding is closely related to polymorphism. In Python, dynamic binding is the process of resolving a method or attribute at runtime, instead of at compile time.

    According to the polymorphism feature, different objects respond differently to the same method call based on their implementations. This behavior is achieved through method overriding, where a subclass provides its implementation of a method defined in its superclass.

    The Python interpreter determines which is the appropriate method or attribute to invoke based on the object’s type or class hierarchy at runtime. This means that the specific method or attribute to be called is determined dynamically, based on the actual type of the object.

    Example

    The following example illustrates dynamic binding in Python −

    classshape:defdraw(self):print("draw method")returnclasscircle(shape):defdraw(self):print("Draw a circle")returnclassrectangle(shape):defdraw(self):print("Draw a rectangle")return
    
    shapes =[circle(), rectangle()]for shp in shapes:
       shp.draw()

    It will produce the following output −

    Draw a circle
    Draw a rectangle
    

    As you can see, the draw() method is bound dynamically to the corresponding implementation based on the object’s type. This is how dynamic binding is implemented in Python.

    Duck Typing

    Another concept closely related to dynamic binding is duck typing. Whether an object is suitable for a particular use is determined by the presence of certain methods or attributes, rather than its type. This allows for greater flexibility and code reuse in Python.

    Duck typing is an important feature of dynamic typing languages like Python (PerlRubyPHPJavascript, etc.) that focuses on an object’s behavior rather than its specific type. According to the “duck typing” concept, “If it walks like a duck and quacks like a duck, then it must be a duck.”

    Duck typing allows objects of different types to be used interchangeably as long as they have the required methods or attributes. The goal is to promote flexibility and code reuse. It is a broader concept that emphasizes object behavior and interface rather than formal types.

    Here is an example of duck typing −

    classcircle:defdraw(self):print("Draw a circle")returnclassrectangle:defdraw(self):print("Draw a rectangle")returnclassarea:defarea(self):print("calculate area")returndefduck_function(obj):
       obj.draw()
    
    objects =[circle(), rectangle(), area()]for obj in objects:
       duck_function(obj)

    It will produce the following output −

    Draw a circle
    Draw a rectangle
    Traceback (most recent call last):
     File "C:\Python311\hello.py", line 21, in <module>
      duck_function(obj)
     File "C:\Python311\hello.py", line 17, in duck_function
     obj.draw()
    AttributeError: 'area' object has no attribute 'draw'
    

    The most important idea behind duck typing is that the duck_function() doesn’t care about the specific types of objects it receives. It only requires the objects to have a draw() method. If an object “quacks like a duck” by having the necessary behavior, it is treated as a “duck” for the purpose of invoking the draw() method.

    Thus, in duck typing, the focus is on the object’s behavior rather than its explicit type, allowing different types of objects to be used interchangeably as long as they exhibit the required behavior.

  • Python – Method Overloading

    Method overloading is a feature of object-oriented programming where a class can have multiple methods with the same name but different parameters. To overload method, we must change the number of parameters or the type of parameters, or both.

    Method Overloading in Python

    Unlike other programming languages like Java, C++, and C#, Python does not support the feature of method overloading by default. However, there are alternative ways to achieve it.

    Example

    If you define a method multiple times as shown in the below code, the last definition will override the previous ones. Therefore, this way of achieving method overloading in Python generates error.

    classexample:defadd(self, a, b):
          x = a+b
          return x
       defadd(self, a, b, c):
          x = a+b+c
          return x
    
    obj = example()print(obj.add(10,20,30))print(obj.add(10,20))

    The first call to add() method with three arguments is successful. However, calling add() method with two arguments as defined in the class fails.

    60
    Traceback (most recent call last):
     File "C:\Users\user\example.py", line 12, in <module>
      print (obj.add(10,20))
             ^^^^^^^^^^^^^^
    TypeError: example.add() missing 1 required positional argument: 'c'
    

    The output tells you that Python considers only the latest definition of add() method, discarding the earlier definitions.

    To simulate method overloading, we can use a workaround by defining default value to method arguments as None, so that it can be used with one, two or three arguments.

    Example

    The below example shows how to achieve method overloading in Python −

    classexample:defadd(self, a =None, b =None, c =None):
          x=0if a !=Noneand b !=Noneand c !=None:
             x = a+b+c
          elif a !=Noneand b !=Noneand c ==None:
             x = a+b
          return x
    
    obj = example()print(obj.add(10,20,30))print(obj.add(10,20))

    It will produce the following output −

    60
    30
    

    With this workaround, we are able to incorporate method overloading in Python class.

    Implement Method Overloading Using MultipleDispatch

    Python’s standard library doesn’t have any other provision for implementing method overloading. However, we can use a dispatch function from a third-party module named MultipleDispatch for this purpose.

    First, you need to install the Multipledispatch module using the following command −

    pip install multipledispatch
    

    This module has a @dispatch decorator. It takes the number of arguments to be passed to the method to be overloaded. Define multiple copies of add() method with @dispatch decorator as below −

    Example

    In this example, we are using multipledispatch to overload a method in Python.

    from multipledispatch import dispatch
    classexample:@dispatch(int,int)defadd(self, a, b):
          x = a+b
          return x
       @dispatch(int,int,int)defadd(self, a, b, c):
          x = a+b+c
          return x
    
    obj = example()print(obj.add(10,20,30))print(obj.add(10,20))

    Output

    60
    30
  • Python – Method Overriding

    Method Overriding in Python

    The Python method overriding refers to defining a method in a subclass with the same name as a method in its superclass. In this case, the Python interpreter determines which method to call at runtime based on the actual object being referred to.

    You can always override your parent class methods. One reason for overriding parent’s methods is that you may want special or different functionality in your subclass.

    Example

    In the code below, we are overriding a method named myMethod of Parent class.

    # define parent classclassParent:defmyMethod(self):print('Calling parent method')# define child classclassChild(Parent):defmyMethod(self):print('Calling child method')# instance of child
    c = Child()# child calls overridden method
    c.myMethod()

    When the above code is executed, it produces the following output −

    Calling child method
    

    To understand Method Overriding in Python, let us take another example. We use following Employee class as parent class −

    classEmployee:def__init__(self,nm, sal):
          self.name=nm
          self.salary=sal
       defgetName(self):return self.name
       defgetSalary(self):return self.salary
    

    Next, we define a SalesOfficer class that uses Employee as parent class. It inherits the instance variables name and salary from the parent. Additionally, the child class has one more instance variable incentive.

    We shall use built-in function super() that returns reference of the parent class and call the parent constructor within the child constructor __init__() method.

    classSalesOfficer(Employee):def__init__(self,nm, sal, inc):super().__init__(nm,sal)
          self.incnt=inc
       defgetSalary(self):return self.salary+self.incnt
    

    The getSalary() method is overridden to add the incentive to salary.

    Example

    Declare the object of parent and child classes and see the effect of overriding. Complete code is below −

    classEmployee:def__init__(self,nm, sal):
          self.name=nm
          self.salary=sal
       defgetName(self):return self.name
       defgetSalary(self):return self.salary
    
    classSalesOfficer(Employee):def__init__(self,nm, sal, inc):super().__init__(nm,sal)
          self.incnt=inc
       defgetSalary(self):return self.salary+self.incnt
    
    e1=Employee("Rajesh",9000)print("Total salary for {} is Rs {}".format(e1.getName(),e1.getSalary()))
    s1=SalesOfficer('Kiran',10000,1000)print("Total salary for {} is Rs {}".format(s1.getName(),s1.getSalary()))

    When you execute this code, it will produce the following output −

    Total salary for Rajesh is Rs 9000
    Total salary for Kiran is Rs 11000
    

    Base Overridable Methods

    The following table lists some generic functionality of the object class, which is the parent class for all Python classes. You can override these methods in your own class −

    Sr.NoMethod, Description & Sample Call
    1__init__ ( self [,args…] )Constructor (with any optional arguments)Sample Call : obj = className(args)
    2__del__( self )Destructor, deletes an objectSample Call : del obj
    3__repr__( self )Evaluatable string representationSample Call : repr(obj)
    4__str__( self )Printable string representationSample Call : str(obj)
  • Python – Polymorphism

    What is Polymorphism in Python?

    The term polymorphism refers to a function or method taking different forms in different contexts. Since Python is a dynamically typed language, polymorphism in Python is very easily implemented.

    If a method in a parent class is overridden with different business logic in its different child classes, the base class method is a polymorphic method.

    Ways of implementing Polymorphism in Python

    There are four ways to implement polymorphism in Python −

    • Duck Typing
    • Operator Overloading
    • Method Overriding
    • Method Overloading
    implementing polymorphism

    Duck Typing in Python

    Duck typing is a concept where the type or class of an object is less important than the methods it defines. Using this concept, you can call any method on an object without checking its type, as long as the method exists.

    This term is defined by a very famous quote that states: Suppose there is a bird that walks like a duck, swims like a duck, looks like a duck, and quaks like a duck then it probably is a duck.

    Example

    In the code given below, we are practically demonstrating the concept of duck typing.

    classDuck:defsound(self):return"Quack, quack!"classAnotherBird:defsound(self):return"I'm similar to a duck!"defmakeSound(duck):print(duck.sound())# creating instances
    duck = Duck()
    anotherBird = AnotherBird()# calling methods
    makeSound(duck)   
    makeSound(anotherBird)

    When you execute this code, it will produce the following output −

    Quack, quack!
    I'm similar to a duck!
    

    Method Overriding in Python

    In method overriding, a method defined inside a subclass has the same name as a method in its superclass but implements a different functionality.

    Example

    As an example of polymorphism given below, we have shape which is an abstract class. It is used as parent by two classes circle and rectangle. Both classes override parent’s draw() method in different ways.

    from abc import ABC, abstractmethod
    classshape(ABC):@abstractmethoddefdraw(self):"Abstract method"returnclasscircle(shape):defdraw(self):super().draw()print("Draw a circle")returnclassrectangle(shape):defdraw(self):super().draw()print("Draw a rectangle")return
    
    shapes =[circle(), rectangle()]for shp in shapes:
       shp.draw()

    Output

    When you run the above code, it will produce the following output −

    Draw a circle
    Draw a rectangle
    

    The variable shp first refers to circle object and calls draw() method from circle class. In next iteration, it refers to rectangle object and calls draw() method from rectangle class. Hence draw() method in shape class is polymorphic.

    Overloading Operators in Python

    Suppose you have created a Vector class to represent two-dimensional vectors, what happens when you use the plus operator to add them? Most likely Python will yell at you.

    You could, however, define the __add__ method in your class to perform vector addition and then the plus operator would behave as per expectation −

    Example

    classVector:def__init__(self, a, b):
          self.a = a
          self.b = b
    
       def__str__(self):return'Vector (%d, %d)'%(self.a, self.b)def__add__(self,other):return Vector(self.a + other.a, self.b + other.b)
    
    v1 = Vector(2,10)
    v2 = Vector(5,-2)print(v1 + v2)

    When the above code is executed, it produces the following result −

    Vector(7,8)
    

    Method Overloading in Python

    When a class contains two or more methods with the same name but different number of parameters then this scenario can be termed as method overloading.

    Python does not allow overloading of methods by default, however, we can use the techniques like variable-length argument lists, multiple dispatch and default parameters to achieve this.

    Example

    In the following example, we are using the variable-length argument lists to achieve method overloading.

    defadd(*nums):returnsum(nums)# Call the function with different number of parameters
    result1 = add(10,25)
    result2 = add(10,25,35)print(result1)print(result2)

    When the above code is executed, it produces the following result −

    35
    70
  • Multi Level Inheritance in Python

    Inheritance is a feature in object-oriented programming through which a class can inherit attributes and methods from another class. In Python, you can implement different types of inheritance, such as single inheritance, multiple inheritance, and multilevel inheritance. This chapter covers how to implement multilevel inheritance in Python.

    Multilevel Inheritance in Python

    Multilevel inheritance is a type of inheritance that happens hierarchically such that a derived class inherits from another derived class. Meaning, a class is derived from a class which is also derived from another class. This forms a chain of inheritance.

    The class at the top of the chain is called the base class or parent class, the class in the middle is called the derived class or child class, and the class at the bottom of the chain is called the sub-derived class or sub-child class.

    The following illustrations depicts the concept of multilevel inheritance −

    Multilevel Inheritance in Python

    Example of Multilevel Inheritance in Python

    Let’s look at an example to understand how multilevel inheritance works in Python −

    # Base classclassNetwork:defconnectivity(self):return"Network connects"# Derived classclassNetwork_5G(Network):deffast_connectivity(self):return"5G Network provides superfast connectivity"# Sub-derived classclassNetwork_5G_Airtel(Network_5G):deffast_and_stable_connectivity(self):return"Airtel 5G network is fast and remains stable"# Creating an instance of Network_5G_Airtel
    network_object = Network_5G_Airtel()print(network_object.connectivity())# Inherited from Network classprint(network_object.fast_connectivity())# Inherited from Network_5G classprint(network_object.fast_and_stable_connectivity())# Inherited from Network_5G_Airtel class

    In this code, the Network_5G_Airtel class inherits the functions and attributes from the Network_5G class, which in turn inherits from the Network class. So the Network_5G_Airtel will have access to methods such as connectivity()(from Network), fast_connectivity()(from Network_5G), and fast_and_stable_connectivity()(from Network_5G_Airtel itself).

    When you run the code, its output will be −

    Network connects
    5G Network provides superfast connectivity
    Airtel 5G network is fast and remains stable
    

    MRO for Multilevel Inheritance in Python

    Let’s consider a scenario from the above example in which the Network_5G class also have a method named connectivity(). Now, What will happen if we call the connectivity() method from the instance of the Network_5G_Airtel class?. This is where the Method Resolution Order (MRO) comes into play.

    Let’s modify the previous example to include a connectivity() method in the Network_5G class −

    # Base classclassNetwork:defconnectivity(self):return"Network connects"# Derived classclassNetwork_5G(Network):deffast_connectivity(self):return"5G Network provides superfast connectivity"defconnectivity(self):return"5G Network connects faster"# Sub-derived classclassNetwork_5G_Airtel(Network_5G):deffast_and_stable_connectivity(self):return"Airtel 5G network is fast and remains stable"# Creating an instance of Network_5G_Airtel
    obj1 = Network_5G_Airtel()print(obj1.connectivity())# Inherited from Network class

    The output of the above code will be −

    5G Network connects faster
    

    This shows that the connectivity() method from the Network_5G class is called instead of the one from the Network class. This is because the method resolution order (MRO) in Python follows a depth-first approach, meaning it will first look for the method in the current class, then in the parent class, and so on up the hierarchy.

    To know the method resolution order of any class, you can use the mro() method. Here’s how you can do it:

    print(Network_5G_Airtel.mro())
    
    Output:
    [<class '__main__.Network_5G_Airtel'>, <class '__main__.Network_5G'>, 
    <class '__main__.Network'>, <class 'object'>]
    

    Overriding Methods in Multilevel Inheritance

    In multilevel inheritance, a derived class can override methods of its parent class. Meaning, if a method is defined in both the parent class and the child class, the method in the child class will override the one in the parent class.

    Here’s an example to illustrate method overriding in multilevel inheritance −

    # Base classclassVehicle:defstart(self):return"Vehicle starts"# Derived classclassCar(Vehicle):defstart(self):return"Car starts"# Sub-derived classclassSportsCar(Car):defstart(self):return"Sports Car starts"# Creating an instance of SportsCar
    sports_car = SportsCar()print(sports_car.start())# Calls the start method of SportsCar class

    The output of the above code will be −

    Sports Car starts
    

    Conclusion

    To conclude, multilevel inheritance is a type of inheritance where a class is derived from another derived class such that a chain of inheritance is formed. The method resolution order (MRO) will determine which method is called when there are methods with the same name in the inheritance chain. Method overriding can be performed to override the methods of the parent class in the child class.

  • Python – Multiple Inheritance

    The inheritance is a feature in object-oriented programming that allows a class to inherit attributes and methods from another class. In Python, you can implement different types of inheritance, such as single inheritance, multiple inheritance, and multilevel inheritance. This chapter covers multiple inheritance in detail.

    What is Multiple Inheritance?

    Multiple inheritance is a type of inheritance where a single class can inherit attributes and methods from more than one parent class. This can be used when you want to get the functionality of multiple classes into a single class. The image below illustrates multiple inheritance −

    Multiple Inheritance Diagram

    Example of Multiple Inheritance

    In the following example, the Child class inherits from both the Father and Mother classes −

    classFather:defskill1(self):print("Father's skill: Gardening")classMother:defskill2(self):print("Mother's skill: Cooking")classChild(Father, Mother):pass
    
    c = Child()
    c.skill1()
    c.skill2()

    The output of above code will be −

    Father's skill: Gardening
    Mother's skill: Cooking
    

    Explanation: In this example, the Child class have access to both skill1() and skill2() methods from its parent classes. When we create an instance of Child and call skill1(), it executes the method from the Father class. Similarly, when we call skill2(), it executes the method from the Mother class.

    But, what will happen if both parent classes have a method with the same name? Which will be executed? This is where the method resolution order (MRO) comes into play.

    Method Resolution Order for Multiple Inheritance

    If methods with same name are defined in multiple parent classes, Python follows a specific order to decide which method to execute. This order is known as the Method Resolution Order (MRO). This order is determined by the C3 linearization algorithm, also called MRO. The MRO order is listed below.

    • First, Python looks for the method in the child class itself.
    • If not found, it searches the parent classes in the order they are listed.
    • This continues until the method is found or all classes have been searched.

    You can check the MRO of a object using the mro() method or the __mro__ attribute. For example −

    print(Child.mro())
    
    Output:
    [<class '__main__.Child'>, <class '__main__.Father'>, <class '__main__.Mother'>, 
    <class 'object'>]
    

    Example

    In the following example, the Child class inherits skill() method from both the Father and Mother classes, but mother’s method is called.

    classFather:defskill(self):print("Father's skill: Gardening")classMother:defskill(self):print("Mother's skill: Cooking")classChild(Father, Mother):pass
    
    c = Child()
    c.skill()

    Explanation: When the skill method is called on the Child class instance c, it will first look for the method in the Child class. Since the Child class does not have a skill method, it will search the parent classes in the order they are listed (Father, then Mother) and execute the first one it finds.

    The output of above code will be −

    Father's skill: Gardening
    

    Using super() in Multiple Inheritance

    The super() function is used by child class to call a method from a parent class. If you use super() in a method in the case of multiple inheritance, it will strictly follow the MRO order to determine which method to call.

    Example

    In this example, we have a class hierarchy with four classes: A, B, C, and D. Class D inherits from both B and C, demonstrating multiple inheritance.

    classA:defshow(self):print("Class A")classB(A):defshow(self):print("Class B")super().show()classC(A):defshow(self):print("Class C")super().show()classD(B, C):defshow(self):print("Class D")super().show()
    
    d = D()
    d.show()

    The output of the above code will be −

    Class D
    Class B
    Class C
    Class A
    

    Explanation: The MRO for class D is [D, B, C, A, object]. This means that when a method is called on an instance of D, Python will look for the method in D first, then B, then C, and finally A. So, “Class D”, “Class B”, “Class C”, and “Class A” will be printed in that order.

    Diamond Problem in Multiple Inheritance

    The diamond problem is a common issue in multiple inheritance. It arises when two parent classes inherit from the same base class, and a child class inherits from both parent classes. The diagram below shows this scenario:

    Diamond Problem Diagram

    The diamond problem creates a confusion that which method to call from the base class when it is inherited by the child class. Python resolves this by following a the mro (Method Resolution Order) that we discussed earlier. Let’s see an example of the diamond problem −

    Example

    The code below demonstrates the diamond problem and how Python resolves it using MRO −

    classA:defshow(self):print("Class A")classB(A):defshow(self):print("Class B")classC(A):defshow(self):print("Class C")classD(B, C):pass
    
    d = D()
    d.show()

    The output of the above code will be −

    Class B
    

    Explanation: The MRO order for class D is [D, B, C, A]. So, when d.show() is called, it first looks in class D, no show function is found there, so it goes to class B, finds the show function, and executes it.

    Pros and Cons of Multiple Inheritance

    The multiple inheritance has several advantages and disadvantages −

    ProsCons
    Improves code modularity and reusability.Increases complexity and may cause ambiguity.
    Allows combining functionalities from multiple classes.Can lead to the diamond problem.
    Can create complex real-world relationshipsHarder to debug and maintain.

    Conclusion

    Multiple inheritance is object-oriented programming concept where a method from multiple parent classes can be inherited by a child class. We have discussed how Python handles multiple inheritance using Method Resolution Order (MRO) and how to use the super() function. The diamond problem is a common issue in multiple inheritance, but Python resolves it using MRO. The main drawback of multiple inheritance is it makes the class hierarchy more complex and harder to understand.

  • Python – Inheritance

    What is Inheritance in Python?

    Inheritance is one of the most important features of object-oriented programming languages like Python. It is used to inherit the properties and behaviours of one class to another. The class that inherits another class is called a child class and the class that gets inherited is called a base class or parent class.

    If you have to design a new class whose most of the attributes are already well defined in an existing class, then why redefine them? Inheritance allows capabilities of existing class to be reused and if required extended to design a new class.

    Inheritance comes into picture when a new class possesses ‘IS A’ relationship with an existing class. For example, Car IS a vehicle, Bus IS a vehicle, Bike IS also a vehicle. Here, Vehicle is the parent class, whereas car, bus and bike are the child classes.

    inheritance

    Creating a Parent Class

    The class whose attributes and methods are inherited is called as parent class. It is defined just like other classes i.e. using the class keyword.

    Syntax

    The syntax for creating a parent class is shown below −

    classParentClassName:{classbody}

    Creating a Child Class

    Classes that inherit from base classes are declared similarly to their parent class, however, we need to provide the name of parent classes within the parentheses.

    Syntax

    Following is the syntax of child class −

    classSubClassName(ParentClass1[, ParentClass2,...]):{sub classbody}

    Types of Inheritance

    In Python, inheritance can be divided in five different categories −

    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    types of inheritance

    Python – Single Inheritance

    This is the simplest form of inheritance where a child class inherits attributes and methods from only one parent class.

    Example

    The below example shows single inheritance concept in Python −

    # parent classclassParent:defparentMethod(self):print("Calling parent method")# child classclassChild(Parent):defchildMethod(self):print("Calling child method")# instance of child
    c = Child()# calling method of child class
    c.childMethod()# calling method of parent class
    c.parentMethod()

    On running the above code, it will print the following result −

    Calling child method
    Calling parent method
    

    Python – Multiple Inheritance

    Multiple inheritance in Python allows you to construct a class based on more than one parent classes. The Child class thus inherits the attributes and method from all parents. The child can override methods inherited from any parent.

    Syntax

    classparent1:#statementsclassparent2:#statementsclasschild(parent1, parent2):#statements

    Example

    Python’s standard library has a built-in divmod() function that returns a two-item tuple. First number is the division of two arguments, the second is the mod value of the two operands.

    This example tries to emulate the divmod() function. We define two classes division and modulus, and then have a div_mod class that inherits them.

    classdivision:def__init__(self, a,b):
          self.n=a
          self.d=b
       defdivide(self):return self.n/self.d
    classmodulus:def__init__(self, a,b):
          self.n=a
          self.d=b
       defmod_divide(self):return self.n%self.d
          
    classdiv_mod(division,modulus):def__init__(self, a,b):
          self.n=a
          self.d=b
       defdiv_and_mod(self):
          divval=division.divide(self)
          modval=modulus.mod_divide(self)return(divval, modval)

    The child class has a new method div_and_mod() which internally calls the divide() and mod_divide() methods from its inherited classes to return the division and mod values.

    x=div_mod(10,3)print("division:",x.divide())print("mod_division:",x.mod_divide())print("divmod:",x.div_and_mod())

    Output

    division: 3.3333333333333335
    mod_division: 1
    divmod: (3.3333333333333335, 1)
    

    Method Resolution Order (MRO)

    The term method resolution order is related to multiple inheritance in Python. In Python, inheritance may be spread over more than one levels. Let us say A is the parent of B, and B the parent for C. The class C can override the inherited method or its object may invoke it as defined in its parent. So, how does Python find the appropriate method to call.

    Each Python has a mro() method that returns the hierarchical order that Python uses to resolve the method to be called. The resolution order is from bottom of inheritance order to top.

    In our previous example, the div_mod class inherits division and modulus classes. So, the mro method returns the order as follows −

    [<class'__main__.div_mod'>,<class'__main__.division'>,<class'__main__.modulus'>,<class'object'>]

    Python – Multilevel Inheritance

    In multilevel inheritance, a class is derived from another derived class. There exists multiple layers of inheritance. We can imagine it as a grandparent-parent-child relationship.

    Example

    In the following example, we are illustrating the working of multilevel inheritance.

    # parent classclassUniverse:defuniverseMethod(self):print("I am in the Universe")# child classclassEarth(Universe):defearthMethod(self):print("I am on Earth")# another child classclassIndia(Earth):defindianMethod(self):print("I am in India")# creating instance 
    person = India()# method calls
    person.universeMethod() 
    person.earthMethod() 
    person.indianMethod()

    When we execute the above code, it will produce the following result −

    I am in the Universe
    I am on Earth
    I am in India
    

    Python – Hierarchical Inheritance

    This type of inheritance contains multiple derived classes that are inherited from a single base class. This is similar to the hierarchy within an organization.

    Example

    The following example illustrates hierarchical inheritance. Here, we have defined two child classes of Manager class.

    # parent classclassManager:defmanagerMethod(self):print("I am the Manager")# child classclassEmployee1(Manager):defemployee1Method(self):print("I am Employee one")# second child classclassEmployee2(Manager):defemployee2Method(self):print("I am Employee two")# creating instances 
    emp1 = Employee1()  
    emp2 = Employee2()# method calls
    emp1.managerMethod() 
    emp1.employee1Method()
    emp2.managerMethod() 
    emp2.employee2Method()

    On executing the above program, you will get the following output −

    I am the Manager
    I am Employee one
    I am the Manager
    I am Employee two
    

    Python – Hybrid Inheritance

    Combination of two or more types of inheritance is called as Hybrid Inheritance. For instance, it could be a mix of single and multiple inheritance.

    Example

    In this example, we have combined single and multiple inheritance to form a hybrid inheritance of classes.

    # parent classclassCEO:defceoMethod(self):print("I am the CEO")classManager(CEO):defmanagerMethod(self):print("I am the Manager")classEmployee1(Manager):defemployee1Method(self):print("I am Employee one")classEmployee2(Manager, CEO):defemployee2Method(self):print("I am Employee two")# creating instances 
    emp = Employee2()# method calls
    emp.managerMethod() 
    emp.ceoMethod()
    emp.employee2Method()

    On running the above program, it will give the below result −

    I am the Manager
    I am the CEO
    I am Employee two
    

    The super() function

    In Python, super() function allows you to access methods and attributes of the parent class from within a child class.

    Example

    In the following example, we create a parent class and access its constructor from a subclass using the super() function.

    # parent classclassParentDemo:def__init__(self, msg):
          self.message = msg
    
       defshowMessage(self):print(self.message)# child classclassChildDemo(ParentDemo):def__init__(self, msg):# use of super functionsuper().__init__(msg)# creating instance
    obj = ChildDemo("Welcome to Tutorialspoint!!")
    obj.showMessage()

    On executing, the above program will give the following result −

    Welcome to Tutorialspoint!!
  • Python – Access Modifiers

    The Python access modifiers are used to restrict access to class members (i.e., variables and methods) from outside the class. There are three types of access modifiers namely public, protected, and private.

    • Public members − A class member is said to be public if it can be accessed from anywhere in the program.
    • Protected members − They are accessible from within the class as well as by classes derived from that class.
    • Private members − They can be accessed from within the class only.

    Usually, methods are defined as public and instance variable are private. This arrangement of private instance variables and public methods ensures implementation of principle of encapsulation.

    Access Modifiers in Python

    Unlike C++ and Java, Python does not use the Public, Protected and Private keywords to specify the type of access modifiers. By default, all the variables and methods in a Python class are public.

    Example

    Here, we have Employee class with instance variables name and age. An object of this class has these two attributes. They can be directly accessed from outside the class, because they are public.

    classEmployee:'Common base class for all employees'def__init__(self, name="Bhavana", age=24):
          self.name = name
          self.age = age
    
    e1 = Employee()
    e2 = Employee("Bharat",25)print("Name: {}".format(e1.name))print("age: {}".format(e1.age))print("Name: {}".format(e2.name))print("age: {}".format(e2.age))

    It will produce the following output −

    Name: Bhavana
    age: 24
    Name: Bharat
    age: 25
    

    Python doesn’t enforce restrictions on accessing any instance variable or method. However, Python prescribes a convention of prefixing name of variable/method with single or double underscore to emulate behavior of protected and private access modifiers.

    • To indicate that an instance variable is private, prefix it with double underscore (such as “__age”).
    • To imply that a certain instance variable is protected, prefix it with single underscore (such as “_salary”).

    Another Example

    Let us modify the Employee class. Add another instance variable salary. Make age private and salary as protected by prefixing double and single underscores respectively.

    classEmployee:def__init__(self, name, age, salary):
          self.name = name # public variable
          self.__age = age # private variable
          self._salary = salary # protected variabledefdisplayEmployee(self):print("Name : ", self.name,", age: ", self.__age,", salary: ", self._salary)
    
    e1=Employee("Bhavana",24,10000)print(e1.name)print(e1._salary)print(e1.__age)

    When you run this code, it will produce the following output −

    Bhavana
    10000
    Traceback (most recent call last):
     File "C:\Users\user\example.py", line 14, in <module>
      print (e1.__age)
            ^^^^^^^^
    AttributeError: 'Employee' object has no attribute '__age'
    

    Python displays AttributeError because __age is private, and not available for use outside the class.

    Name Mangling

    Python doesn’t block access to private data, it just leaves for the wisdom of the programmer, not to write any code that access it from outside the class. You can still access the private members by Python’s name mangling technique.

    Name mangling is the process of changing name of a member with double underscore to the form object._class__variable. If so required, it can still be accessed from outside the class, but the practice should be refrained.

    In our example, the private instance variable “__name” is mangled by changing it to the format −

    obj._class__privatevar
    

    So, to access the value of “__age” instance variable of “e1” object, change it to “e1._Employee__age”.

    Change the print() statement in the above program to −

    print(e1._Employee__age)

    It now prints 24, the age of e1.

    Python Property Object

    Python’s standard library has a built-in property() function. It returns a property object. It acts as an interface to the instance variables of a Python class.

    The encapsulation principle of object-oriented programming requires that the instance variables should have a restricted private access. Python doesn’t have efficient mechanism for the purpose. The property() function provides an alternative.

    The property() function uses the getter, setter and delete methods defined in a class to define a property object for the class.

    Syntax

    property(fget=None, fset=None, fdel=None, doc=None)

    Parameters

    • fget − an instance method that retrieves value of an instance variable.
    • fset − an instance method that assigns value to an instance variable.
    • fdel − an instance method that removes an instance variable
    • fdoc − Documentation string for the property.

    The function uses getter and setter methods to return the property object.

    Getters and Setter Methods

    A getter method retrieves the value of an instance variable, usually named as get_varname, whereas the setter method assigns value to an instance variable − named as set_varname.

    Example

    Let us define getter methods get_name() and get_age(), and setters set_name() and set_age() in the Employee class.

    classEmployee:def__init__(self, name, age):
          self.__name = name
          self.__age = age
    
       defget_name(self):return self.__name
       defget_age(self):return self.__age
       defset_name(self, name):
          self.__name = name
          returndefset_age(self, age):
          self.__age=age
    
    e1=Employee("Bhavana",24)print("Name:", e1.get_name(),"age:", e1.get_age())
    e1.set_name("Archana")
    e1.set_age(21)print("Name:", e1.get_name(),"age:", e1.get_age())

    It will produce the following output −

    Name: Bhavana age: 24
    Name: Archana age: 21
    

    The getter and setter methods can retrieve or assign value to instance variables. The property() function uses them to add property objects as class attributes.

    The name property is defined as −

    name =property(get_name, set_name,"name")

    Similarly, you can add the age property −

    age =property(get_age, set_age,"age")

    The advantage of the property object is that you can use to retrieve the value of its associated instance variable, as well as assign value.

    For example,

    print(e1.name) displays value of e1.__name
    e1.name ="Archana" assigns value to e1.__age
    

    Example

    The complete program with property objects and their use is given below −

    classEmployee:def__init__(self, name, age):
          self.__name = name
          self.__age = age
    
       defget_name(self):return self.__name
       defget_age(self):return self.__age
       defset_name(self, name):
          self.__name = name
          returndefset_age(self, age):
          self.__age=age
          return
       name =property(get_name, set_name,"name")
       age =property(get_age, set_age,"age")
    
    e1=Employee("Bhavana",24)print("Name:", e1.name,"age:", e1.age)
    
    e1.name ="Archana"
    e1.age =23print("Name:", e1.name,"age:", e1.age)

    It will produce the following output −

    Name: Bhavana age: 24
    Name: Archana age: 23