<jupyter_start><jupyter_text>All the IPython Notebooks in this lecture series by Dr. <NAME> are available @ **[GitHub](https://github.com/milaan9/06_Python_Object_Class)**  Python InheritanceThe **process of inheriting the properties of the base (or parent) class into a derived (or child) class is called inheritance**. Inheritance enables us to define a class that takes all the functionality from a base class and allows us to add more. In this tutorial, you will learn to use inheritance in Python.  Inheritance in PythonInheritance is a powerful feature in object oriented programming. The main purpose of inheritance is the reusability of code because we can use the existing class to create a new class instead of creating it from scratch.It refers to defining a new [**class**](https://github.com/milaan9/06_Python_Object_Class/blob/main/002_Python_Classes_and_Objects.ipynb) with little or no modification to an existing class. The new class is called **derived (or child)** class and the one from which it inherits is called the **base (or parent)** class.In inheritance, the derived class acquires and access all the data members, properties, and functions from the base class. Also, a derived class can also provide its specific implementation to the methods of the base class. **Syntax:**```pythonclass BaseClass:    Body of base classclass DerivedClass(BaseClass):    Body of derived class```  Types Of InheritanceIn Python, based upon the number of child and parent classes involved, there are five types of inheritance. The type of inheritance are listed below:1. Single Inheritance2. Multiple Inheritance3. Multilevel inheritance4. Hierarchical Inheritance5. Hybrid InheritanceNow let’s see each in detail with example.  Python Single InheritanceIn single inheritance, a derived class inherits from a single-base class. Here in one derived class and one base class.Here, the **`Derived`** class is derived from **`Base`** class.<jupyter_code># Example 1: Single Inheritance

# Base class
class Vehicle:
    def Vehicle_info(self):
        print('Inside Vehicle class')

# Derived class
class Car(Vehicle):
    def car_info(self):
        print('Inside Car class')

# Create object of Car
car = Car()

# access Vehicle's info using car object
car.Vehicle_info()
car.car_info()<jupyter_output>Inside Vehicle class
Inside Car class<jupyter_text>Python Multiple InheritanceIn multiple inheritance, one derived class can inherit from multiple base classes. Here, the **`MultiDerived`** class is derived from **`Base1`** and **`Base2`** classes.<jupyter_code># Example 1: Multiple Inheritance

# Base class 1
class Person:
    def person_info(self, name, age):
        print('Inside Person class')
        print('Name:', name, 'Age:', age)

# Base class 2
class Company:
    def company_info(self, company_name, location):
        print('Inside Company class')
        print('Name:', company_name, 'location:', location)

# Derived class
class Employee(Person, Company):
    def Employee_info(self, salary, skill):
        print('Inside Employee class')
        print('Salary:', salary, 'Skill:', skill)

# Create object of Employee
emp = Employee()

# access data
emp.person_info('Milaan', 33)
emp.company_info('Google', 'Atlanta')
emp.Employee_info(19000, 'Machine Learning')
# Example 2:

class First(object):  # Base class1
    def __init__(self):
        super(First, self).__init__()
        print("first")

class Second(object):  # Base class2
    def __init__(self):
        super(Second, self).__init__()
        print("second")

class Third(Second, First):  # Derived class derived from Base class 1 and Base class 2
    def __init__(self):
        super(Third, self).__init__()
        print("third")

Third(); #call Third class constructor
# Example 3:

class Mammal(object):
    def __init__(self, mammalName):
        print(mammalName, 'is a warm-blooded animal.')
    
class Dog(Mammal):
    def __init__(self):
        print('Dog has four legs.')
        super().__init__('Dog')
    
d1 = Dog()<jupyter_output>Dog has four legs.
Dog is a warm-blooded animal.<jupyter_text>**Explanation**:Here, we called the **`__init__()`** method of the **`Mammal`** class (from the **`Dog`** class) using code**`super().__init__('Dog')`**instead of**`Mammal.__init__(self, 'Dog')`**Since we do not need to specify the name of the base class when we call its members, we can easily change the base class name (if we need to).```python changing base class to CanidaeFamilyclass Dog(CanidaeFamily):  def __init__(self):    print('Dog has four legs.')     no need to change this    super().__init__('Dog')``` The **[super()](https://github.com/milaan9/04_Python_Functions/blob/main/002_Python_Functions_Built_in/068_Python_super%28%29.ipynb)** built-in function returns a proxy object, a substitute object that can call methods of the base class via delegation. This is called indirection (ability to reference base object with **`super()`** built-in function).Since the indirection is computed at the runtime, we can use different base classes at different times (if we need to).<jupyter_code># Example 4:

class Animal:
    def __init__(self, Animal):
        print(Animal, 'is an animal.');

class Mammal(Animal):  # Mammal derived to Animal
    def __init__(self, mammalName):
        print(mammalName, 'is a warm-blooded animal.')
        super().__init__(mammalName)
    
class NonWingedMammal(Mammal):  # NonWingedMammal derived from Mammal (derived from Animal)
    def __init__(self, NonWingedMammal):
        print(NonWingedMammal, "can't fly.")
        super().__init__(NonWingedMammal)

class NonMarineMammal(Mammal):  # NonMarineMammal derived from Mammal (derived from Animal)
    def __init__(self, NonMarineMammal):
        print(NonMarineMammal, "can't swim.")
        super().__init__(NonMarineMammal)

class Dog(NonMarineMammal, NonWingedMammal):  # Dog derived from NonMarineMammal and NonWingedMammal
    def __init__(self):
        print('Dog has 4 legs.');
        super().__init__('Dog')
    
d = Dog()
print('')
bat = NonMarineMammal('Bat')<jupyter_output>Dog has 4 legs.
Dog can't swim.
Dog can't fly.
Dog is a warm-blooded animal.
Dog is an animal.

Bat can't swim.
Bat is a warm-blooded animal.
Bat is an animal.<jupyter_text>Why `super()` keywordThe **`super()`** function is most commonly used with **`__init__`** function in base class. This is usually the only place where we need to do some things in a child then complete the **initialization** in the **parent**.```pythonclass Child(Parent):    def __init__(self, stuff)        self.stuff = stuff        super(Child, self).__init__()```  Private members of parent classWe don’t always want the instance variables of the parent class to be inherited by the child class i.e. we can make some of the instance variables of the parent class private, which won’t be available to the child class.  We can make an instance variable by adding double underscores before its name.<jupyter_code>## Example:

# Python program to demonstrate private members
# of the parent class
class C(object):
    def __init__(self):
            self.c = 21
            # d is private instance variable
            self.__d = 42  # Note: before 'd' there are two '_'

class D(C):
    def __init__(self):
            self.e = 84
            C.__init__(self)

object1 = D()
# produces an error as d is private instance variable
print(D.d)<jupyter_output><empty_output><jupyter_text>Python Multilevel InheritanceIn multilevel inheritance, we can also inherit from a derived class. It can be of any depth in Python.All the features of the base class and the derived class are inherited into the new derived class.For example, there are three classes A, B, C. A is the superclass, B is the child class of A, C is the child class of B. In other words, we can say a **chain of classes** is called multilevel inheritance.Here, the **`Derived1`** class is derived from the **`Base`** class, and the **`Derived2`** class is derived from the **`Derived1`** class.<jupyter_code># Example 1:

# Base class
class Vehicle:
    def Vehicle_info(self):
        print('Inside Vehicle class')

# Child class
class Car(Vehicle):
    def car_info(self):
        print('Inside Car class')

# Child class
class SportsCar(Car):
    def sports_car_info(self):
        print('Inside SportsCar class')

# Create object of SportsCar
s_car = SportsCar()

# access Vehicle's and Car info using SportsCar object
s_car.Vehicle_info()
s_car.car_info()
s_car.sports_car_info()

# Example 2:

class Animal:  # grandparent class
    def eat(self):
        print('Eating...')

class Dog(Animal):  # parent class
    def bark(self):
        print('Barking...')

class BabyDog(Dog):  # child class
    def weep(self):
        print('Weeping...')

d=BabyDog()
d.eat()
d.bark()
d.weep()<jupyter_output>Eating...
Barking...
Weeping...<jupyter_text>Hierarchical InheritanceIn Hierarchical inheritance, more than one child class is derived from a single parent class. In other words, we can say one base class and multiple derived classes.<jupyter_code># Example 1:

class Vehicle:
    def info(self):
        print("This is Vehicle")

class Car(Vehicle):
    def car_info(self, name):
        print("Car name is:", name)

class Truck(Vehicle):
    def truck_info(self, name):
        print("Truck name is:", name)

obj1 = Car()
obj1.info()
obj1.car_info('BMW')

obj2 = Truck()
obj2.info()
obj2.truck_info('Ford')<jupyter_output>This is Vehicle
Car name is: BMW
This is Vehicle
Truck name is: Ford<jupyter_text>Hybrid InheritanceWhen inheritance is consists of multiple types or a combination of different inheritance is called hybrid inheritance.<jupyter_code># Example 1:

class Vehicle:
    def vehicle_info(self):
        print("Inside Vehicle class")

class Car(Vehicle):
    def car_info(self):
        print("Inside Car class")

class Truck(Vehicle):
    def truck_info(self):
        print("Inside Truck class")

# Sports Car can inherits properties of Vehicle and Car
class SportsCar(Car, Vehicle):
    def sports_car_info(self):
        print("Inside SportsCar class")

# create object
s_car = SportsCar()

s_car.vehicle_info()
s_car.car_info()
s_car.sports_car_info()<jupyter_output>Inside Vehicle class
Inside Car class
Inside SportsCar class<jupyter_text>Example of Inheritance in PythonTo demonstrate the use of inheritance, let us take an example.A polygon is a closed figure with 3 or more sides. Say, we have a class called **`Polygon`** defined as follows.<jupyter_code>class Polygon:
    def __init__(self, no_of_sides):
        self.n = no_of_sides
        self.sides = [0 for i in range(no_of_sides)]

    def inputSides(self):
        self.sides = [float(input("Enter side "+str(i+1)+" : ")) for i in range(self.n)]

    def dispSides(self):
        for i in range(self.n):
            print("Side",i+1,"is",self.sides[i])<jupyter_output><empty_output><jupyter_text>This class has data attributes to store the number of sides **`n`** and magnitude of each side as a list called **`sides`**.The **`inputSides()`** method takes in the magnitude of each side and **`dispSides()`** displays these side lengths.A triangle is a polygon with 3 sides. So, we can create a class called **`Triangle`** which inherits from **`Polygon`**. This makes all the attributes of **`Polygon`** class available to the **`Triangle`** class.We don't need to define them again (code reusability). **`Triangle`** can be defined as follows.<jupyter_code>class Triangle(Polygon):
    def __init__(self):
        Polygon.__init__(self,3)

    def findArea(self):
        a, b, c = self.sides
        # calculate the semi-perimeter
        s = (a + b + c) / 2
        area = (s*(s-a)*(s-b)*(s-c)) ** 0.5
        print('The area of the triangle is %0.2f' %area)<jupyter_output><empty_output><jupyter_text>However, class **`Triangle`** has a new method **`findArea()`** to find and print the area of the triangle. Here is a sample run.<jupyter_code>t = Triangle()
t.inputSides()
t.dispSides()
t.findArea()<jupyter_output>The area of the triangle is 7.48<jupyter_text>We can see that even though we did not define methods like **`inputSides()`** or **`dispSides()`** for class **`Triangle`** separately, we were able to use them.If an attribute is not found in the class itself, the search continues to the base class. This repeats recursively, if the base class is itself derived from other classes.<jupyter_code># Example 1:

class Parent: # define parent class
    parentAttr = 100

    def __init__(self):
        print ("Calling parent constructor")

    def parentMethod(self):
        print ('Calling parent method')

    def setAttr(self, attr):
        Parent.parentAttr = attr

    def getAttr(self):
        print ("Parent attribute :", Parent.parentAttr)

class Child(Parent): # define child class
    def __init__(self):
        print ("Calling child constructor")

    def childMethod(self):
        print ('Calling child method')

c = Child() # instance of child
c.childMethod() # child calls its method
c.parentMethod() # calls parent's method
c.setAttr(200) # again call parent's method
c.getAttr() # again call parent's method<jupyter_output>Calling child constructor
Calling child method
Calling parent method
Parent attribute : 200<jupyter_text>**Explanation:**In the above example, **hierarchical** and **multiple** inheritance exists. Here we created, parent class **`Vehicle`** and two child classes named **`Car`** and **`Truck`** this is hierarchical inheritance.Another is **`SportsCar`** inherit from two parent classes named **`Car`** and **`Vehicle`**. This is multiple inheritance.  Python `super()` functionWhen a class inherits all properties and behavior from the parent class is called inheritance. In such a case, the inherited class is a subclass and the latter class is the parent class.In child class, we can refer to parent class by using the **`super()`** function. **[Python super() function](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/)** returns a temporary object of the parent class that allows us to call a parent class method inside a child class method.Benefits of using the **`super()`** function are:* We are not required to remember or specify the parent class name to access its methods.* We can use the **`super()`** function in both **single** and **multiple** inheritances.* The **`super()`** function support code **reusability** as there is no need to write the entire function<jupyter_code># Example 1:

class Company:
    def company_name(self):
        return 'Google'

class Employee(Company):
    def info(self):
        # Calling the superclass method using super()function
        c_name = super().company_name()
        print("Arthur works at", c_name)

# Creating object of child class
emp = Employee()
emp.info()<jupyter_output>Arthur works at Google<jupyter_text>**Explanation:**In the above example, we create a parent class **`Company`** and child class **`Employee`**. In Employee class, we call the parent class method by using a **`super()`** function.  `issubclass()`In Python, we can verify whether a particular class is a subclass of another class. For this purpose, we can use **[Python issubclass() function](https://github.com/milaan9/04_Python_Functions/blob/main/002_Python_Functions_Built_in/036_Python_issubclass().ipynb)**. This function returns **`True`** if the given class is the subclass of the specified class. Otherwise, it returns **`False`**.**Syntax:**```pythonissubclass(class, classinfo)```Where,* **`class`**: class to be checked.* **`classinfo`**: a class, type, or a tuple of classes or data types.<jupyter_code># Example 1:

class Company:
    def fun1(self):
        print("Inside parent class")

class Employee(Company):
    def fun2(self):
        print("Inside child class.")

class Player:
    def fun3(self):
        print("Inside Player class.")

# Result True
print(issubclass(Employee, Company))

# Result False
print(issubclass(Employee, list))

# Result False
print(issubclass(Player, Company))

# Result True
print(issubclass(Employee, (list, Company)))

# Result True
print(issubclass(Company, (list, Company)))<jupyter_output>True
False
False
True
True<jupyter_text>Method OverridingIn inheritance, all members available in the parent class are by default available in the child class. If the child class does not satisfy with parent class implementation, then the child class is allowed to redefine that method by extending additional functions in the child class. This concept is called **method overriding**.When a child class method has the same name, same parameters, and same return type as a method in its superclass, then the method in the child is said to **override** the method in the parent class.<jupyter_code># Example 1:

class Vehicle:
    def max_speed(self):
        print("max speed is 100 Km/Hour")

class Car(Vehicle):
    # overridden the implementation of Vehicle class
    def max_speed(self):
        print("max speed is 200 Km/Hour")

# Creating object of Car class
car = Car()
car.max_speed()<jupyter_output>max speed is 200 Km/Hour<jupyter_text>**Explanation:**In the above example, we create two classes named **`Vehicle`** (Parent class) and **`Car`** (Child class). The class **`Car`**  extends from the class **`Vehicle`** so, all properties of the parent class are available in the child class. In addition to that, the child class redefined the method **`max_speed()`**.<jupyter_code># Example 2:

class Parent: # define parent class
    def myMethod(self):
        print ('Calling parent method')


class Child(Parent): # define child class
    def myMethod(self):
        print ('Calling child method')


c = Child() # instance of child
c.myMethod() # child calls overridden method
# Example 3:

# parent class
class Bird:
    
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# child class
class Penguin(Bird):

    def __init__(self):
        # call super() function to run the __init__() method of the parent class inside the child class.
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()
peggy.swim()
peggy.run()

#issubclass(Penguin, Bird) 
isinstance(peggy, Bird)<jupyter_output>Bird is ready
Penguin is ready
Penguin
Swim faster
Run faster<jupyter_text>Example of Method Overriding in PythonIn the example of **"Polygon"** and **"Triangle"**, notice that **`__init__()`** method was defined in both classes, **`Triangle`** as well **`Polygon`**. When this happens, the method in the derived class overrides that in the base class. This is to say, **`__init__()`** in **`Triangle`** gets preference over the **`__init__`** in **`Polygon`**.Generally when overriding a base method, we tend to extend the definition rather than simply replace it. The same is being done by calling the method in base class from the one in derived class (calling **`Polygon.__init__()`** from **`__init__()`** in **`Triangle`**). A better option would be to use the built-in function **`super()`**. So, **`super().__init__(3)`** is equivalent to **`Polygon.__init__(self,3)`** and is preferred. To learn more about the **`super()`** function in Python, visit **[Python super() function](https://rhettinger.wordpress.com/2011/05/26/super-considered-super/)**. Two built-in functions **`isinstance()`** and **`issubclass()`** are used to check inheritances.The function **`isinstance()`** returns **`True`** if the object is an instance of the class or other classes derived from it. Each and every class in Python inherits from the base class object.<jupyter_code>isinstance(t,Triangle)
isinstance(t,Polygon)
isinstance(t,int)
isinstance(t,object)<jupyter_output><empty_output><jupyter_text>Similarly, **`issubclass()`** is used to check for class inheritance.<jupyter_code>issubclass(Polygon,Triangle)
issubclass(Triangle,Polygon)
issubclass(bool,int)<jupyter_output><empty_output><jupyter_text>Also, see **[Python isinstance()](https://github.com/milaan9/04_Python_Functions/blob/main/002_Python_Functions_Built_in/034_Python_isinstance%28%29.ipynb)**.  Method Resolution Order in PythonIn Python, Method Resolution Order(MRO) is the order by which **Python looks for a method or attribute**. First, the method or attribute is searched within a class, and then it follows the order we specified while inheriting.This order is also called the Linearization of a class, and a set of rules is called MRO (Method Resolution Order). The **MRO plays an essential role in multiple inheritances as a single method may found in multiple parent classes**.In multiple inheritance, the following search order is followed.1. First, it searches in the current parent class if not available, then searches in the parents class specified while inheriting (that is left to right.)2. We can get the MRO of a class. For this purpose, we can use either the **`mro`** attribute or the **`mro()`** method. **Example:**```pythonclass A:    def process(self):        print(" In class A")class B(A):    def process(self):        print(" In class B")class C(B, A):    def process(self):        print(" In class C") Creating object of C classC1 = C()C1.process()print(C.mro()) In class C [, , , ]``` **Explanation:**In the above example, we create three classes named **`A`**, **`B`** and **`C`**. Class **`B`** is inherited from **`A`**, class **`C`** inherits from **`B`** and **`A`**. When we create an object of the **`C`** class and calling the **`process()`** method, Python looks for the **`process()`** method in the current class in the **`C`** class itself.Then search for parent classes, namely **`B`** and **`A`**, because **`C`** class inherit from **`B`** and **`A`**. that is, **`C(B, A)`** and always search in **left to right manner**.<jupyter_code># Output: True
print(issubclass(list,object))
# Output: True
print(isinstance(5.5,object))
# Output: True
print(isinstance("Hello",object))<jupyter_output>True<jupyter_text>In the multiple inheritance scenario, any specified attribute is searched first in the current class. If not found, the search continues into parent classes in depth-first, left-right fashion without searching the same class twice.So, in the above example of **`MultiDerived`** class the search order is [**`MultiDerived`**, **`Base1`**, **`Base2`**, **`object`**]. This order is also called linearization of **`MultiDerived`** class and the set of rules used to find this order is called **Method Resolution Order (MRO)**.MRO must prevent local precedence ordering and also provide monotonicity. It ensures that a class always appears before its parents. In case of multiple parents, the order is the same as tuples of base classes.MRO of a class can be viewed as the **`__mro__`** attribute or the **`mro()`** method. The former returns a tuple while the latter returns a list```python>>> MultiDerived.__mro__(, , , )>>> MultiDerived.mro()[, , , ]``` Here is a little more complex multiple inheritance example and its visualization along with the MRO.<jupyter_code># Demonstration of MRO

class X:
    pass


class Y:
    pass


class Z:
    pass


class A(X, Y):
    pass


class B(Y, Z):
    pass


class M(B, A, Z):
    pass

# Output:
# [<class '__main__.M'>, <class '__main__.B'>,
#  <class '__main__.A'>, <class '__main__.X'>,
#  <class '__main__.Y'>, <class '__main__.Z'>,
#  <class 'object'>]

print(M.mro())<jupyter_output>[<class '__main__.M'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class 'object'>]
{'fraction_non_alphanumeric': 0.11411829180894566, 'fraction_numerical': 0.0067029214619957, 'mean_word_length': 4.458352508053382, 'pattern_counts': {'":': 0, '<': 78, '<?xml version=': 0, '>': 84, 'https://': 7, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '152653', 'license': 'Unlicense', 'n_tokens_mistral': 7264, 'n_tokens_neox': 6872, 'n_words': 2772, 'path': '003_Python_Inheritance.ipynb', 'repo_name': 'RootWu0/06_Python_Object_Class', 'size': '23727'}