# Inheritance in Python

Inheritance is a fundamental concept in object-oriented programming that allows you to create new classes based on existing classes, enabling code reuse and promoting a hierarchical structure.

### Single Inheritance:

* Single inheritance refers to a scenario where a class inherits from a single base class.
    
* The derived class acquires the attributes and methods of the base class, allowing for code reuse and extension of functionality.
    

```python
class Creatures:
    def character(self):
        print("They Cannot talk")

class Bird(Creatures):
    def feature(self):
        print("Bird Can Fly")

# Creating an instance of Dog
bird = Bird()
bird.feature()  # Output: Bird Fly
bird.character() # Output: They Cannot talk
```

In this example, the `Bird` class inherits from the `Creatures` class using single inheritance. So, I can easily use the properties of the Creatures class inside the **Bird** class. For instance, `bird. character()` in the above example.

### Multilevel Inheritance

Multilevel inheritance involves a chain of inheritance where a derived class inherits from a base class, and another class inherits from the derived class.

```python
class Vehicle:
    def drive(self):
        print("Help in Driving")

class Car(Vehicle):
    def __init__(self, color, speed):
        self.color = color
        self.speed = speed
        
    def feature(self):
        print(f"Its color is {self.color} and speed is{self.speed}")

class SportsCar(Car):
    def __init__(self, color, speed, price):
        super().__init__(color, speed)# overriding init method of Car
        self.price = price


car = Car('black','100km/hr')
sport_car = SportsCar('red','200km/hr','15000$')

car.drive()
sport_car.feature() # accessing Car method
sport_car.drive() #accessing Vehicle method
```

**Output :**

```python
Help in Driving
Its color is red and speed is200km/hr
Help in Driving
```

In this example, the `SportsCar` class inherits from the`Car` class, which itself inherits from the `Vehicle` class. The `SportsCar` class inherits the `drive` method from the `Vehicle` class through the intermediate `Car` class.

### super() keyword

* `super()` is a built-in function that allows you to call a method from a parent class within a subclass.
    
* It provides a convenient way to invoke the parent class's methods and access its attributes.
    
* In the above example of multilevel inheritance, I am using **super().\_\_init\_\_(color, speed)** in the class **SportsCar** to override attributes from **Car** class.
    
    ```python
    class SportsCar(Car):
        def __init__(self, color, speed, price):
            super().__init__(color, speed)#overriding init method of Car
            self.price = price
    
    '''THIS CODE IS EQUIVALENT TO BELOW CODE'''
    
    class SportsCar(Car):
        def __init__(self, color, speed, price):
            self.color = color
            self.speed = speed
            self.price = price
    ```
    

### Multiple Inheritance

* Multiple inheritance allows a class to inherit from more than one base class.
    
* The derived class inherits attributes and methods from all the base classes, enabling it to combine the properties and behaviors of multiple classes. Let's explore an example:
    

```python
class Vehicle:
    def drive(self):
        print("Driving")

class Car:
    def __init__(self, color, speed):
        self.color = color
        self.speed = speed

    def feature(self):
        print(f"It color is {self.color} and speed is{self.speed}")

class SportsCar(Vehicle, Car):
    def __init__(self, color, speed, price):
        super().__init__(color, speed)#overriding attribute of Car class
        self.price = price


car = Car('black','100km/hr')
sport_car = SportsCar('red','200km/hr','15000$')
sport_car.drive()  # Output: Driving
```

In this example, the `Duck` class inherits from both the `Flyer` and `Swimmer` classes using multiple inheritances. The `Duck` class can access the methods of both base classes.

### MRO algorithm

* MRO (Method Resolution Order) is a process in Python that determines the order in which methods are resolved or searched for in a class hierarchy.
    
* It is used when multiple inheritance is involved, and it helps avoid ambiguities and conflicts in method resolution.
    
    ```python
    class A:
        def greet(self):
            print("Hello from A")
    
    class B(A):
        def greet(self):
            print("Hello from B")
    
    class C(A):
        def greet(self):
            print("Hello from C")
    
    class D(B, C):
        pass
    
    # Creating an instance of D
    d = D()
    d.greet()
    print(d.greet())
    print(D.mro())
    ```
    
    **Output :**
    
    ```python
    Hello from B
    [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
     <class '__main__.A'>, <class 'object'>]
    ```
    
* If we want to access the greet() method making an object for D then first it will check it in **class D and** then similarly in **class B, C, and A** respectively according to `mro` algorithm as in the above example.
    
* We are sure that we are creating the object of class D.So, it first checks in class D. But between **class B and class C, class B** is on the left of **class C**. Therefore, it accesses the method from **class B** based on `mro()` algorithm.
