Inheritance in Python

Spread the love

Python, a versatile and powerful programming language, stands out in its support for Object-Oriented Programming (OOP). One of the core tenets of OOP that Python deftly encapsulates is inheritance. This article unfolds the concept of inheritance in Python, touching upon its types, special functions, advantages, and disadvantages.

1. Classes and Objects:

What is a Class?

A class in programming represents a blueprint or a template for creating objects. It provides the structure for storing data (attributes) and methods to manipulate that data or perform operations. The class defines what an object will contain and what operations can be performed on the created object.

In essence, think of a class as a prototype or a mold. Just like a mold can be used to produce multiple similar objects, a class is utilized to produce multiple instances with similar properties and behaviors.

Syntax:

class ClassName:
    # Attributes and Methods

Attributes and Methods:

  • Attributes: These are variables contained within the class. They store data specific to an instance of the class.
  • Methods: These are functions contained within the class. They define the behaviors or operations that can be performed on the class’s attributes.

Example:

Let’s create a simple class Dog which has an attribute name and a method bark.

class Dog:
    # Initializer or Constructor
    def __init__(self, name):
        self.name = name

    # Method
    def bark(self):
        return f"{self.name} says Woof!"

What is an Object?

Objects are individual instances of a class. If you think of the class as the blueprint, the object is the realized version of that blueprint – the tangible result. Every object will have its own copy of attributes defined in the class, and it can perform operations using the methods defined in the class.

Creating an Object (Instantiation): To create an object of a class, you call the class as if you’re calling a function. The resulting object will be an instance of that class.

Example: Using the previously defined Dog class, let’s create a few objects.

# Creating objects of the Dog class
dog1 = Dog("Buddy")
dog2 = Dog("Charlie")

# Accessing attributes and methods
print(dog1.name)      # Outputs: Buddy
print(dog2.bark())    # Outputs: Charlie says Woof!

Understanding self :

In our class definition, you might’ve noticed the self parameter in the methods. self refers to the instance of the class itself. It’s how we access and modify object-specific data in the class. Every method in a class must have self as its first parameter, even if you don’t explicitly use it.

Initializers (__init__ method):

The __init__ method in a class is a special method called an initializer (commonly referred to as a constructor in other programming languages). It’s automatically called when an object of the class is instantiated. Its primary purpose is to initialize the attributes of the object.

2. What is Inheritance in Python?

Inheritance is a fundamental OOP concept where a new class can inherit properties and methods from an existing class. The existing class is called the parent or base class, and the new class is termed the child or derived class. Inheritance fosters code reusability and establishes a form of hierarchy, allowing for more organized and modular code.

3. Types of Inheritance in Python

a. Single Inheritance in Python:

Definition:

Single inheritance is the simplest form of inheritance in object-oriented programming. In this scenario, a derived class (or child class) inherits attributes and methods from only one base class (or parent class).

How It Works:

In single inheritance, the derived class inherits members (attributes and methods) of a single base class. This means that the derived class can access and modify the inherited attributes and methods, and it can also introduce its attributes and methods.

Example:

Imagine a base class named Animal with a method named eat. Now, let’s derive a class Dog from this base class. The derived class Dog will inherit the method eat from the base class and can also introduce its method, say bark.

class Animal:
    def eat(self):
        return "This animal eats food."

class Dog(Animal):
    def bark(self):
        return "Woof! Woof!"

# Creating an object of the derived class Dog
dog_instance = Dog()

print(dog_instance.eat())  # Outputs: This animal eats food.
print(dog_instance.bark()) # Outputs: Woof! Woof!

In this example, even though the Dog class only defines the bark method, it has access to the eat method because it inherits from the Animal class. This demonstrates the essence of single inheritance – the Dog class is an Animal and has all its properties, but it also has its unique properties.

Overriding Methods in Single Inheritance:

In single inheritance, the derived class can provide its implementation of a method that’s already defined in the base class. This is termed as method overriding.

class Animal:
    def sound(self):
        return "Some sound"

class Dog(Animal):
    # Overriding the sound method of the base class
    def sound(self):
        return "Woof! Woof!"

dog_instance = Dog()
print(dog_instance.sound()) # Outputs: Woof! Woof!

In the above code, the Dog class overrides the sound method of the Animal class. When we call the sound method on a Dog object, it uses the method defined in the Dog class, not the one in the Animal class.

b. Multiple Inheritance in Python:

Definition:

Multiple inheritance is a feature in which a class can inherit attributes and methods from more than one base class. In other words, a derived class can have multiple parent classes, thus benefiting from the capabilities of all the parent classes it inherits from.

How It Works:

In multiple inheritance, the derived class merges the attributes and methods of all the parent classes it inherits from. If there are conflicts, like if two parent classes have methods with the same name, Python resolves them using a specific order known as the Method Resolution Order (MRO).

Example:

Consider two base classes, Father and Mother, with the methods work and cook, respectively. Now, let’s derive a class Child that inherits from both Father and Mother.

class Father:
    def work(self):
        return "Working in the office."

class Mother:
    def cook(self):
        return "Cooking in the kitchen."

# Multiple Inheritance: Child inherits from both Father and Mother
class Child(Father, Mother):
    def play(self):
        return "Playing in the park."

# Creating an object of the derived class Child
child_instance = Child()

print(child_instance.work())  # Outputs: Working in the office.
print(child_instance.cook())  # Outputs: Cooking in the kitchen.
print(child_instance.play())  # Outputs: Playing in the park.

Here, the Child class inherits methods from both Father and Mother. Thus, an object of Child can access methods from both parent classes as well as its methods.

Resolving Method Conflicts:

In cases where the inherited parent classes have methods with the same name, Python uses the Method Resolution Order (MRO) to determine which method should be used. The MRO depends on the order in which the base classes are specified during inheritance.

class Father:
    def activity(self):
        return "Working in the office."

class Mother:
    def activity(self):
        return "Cooking in the kitchen."

class Child(Father, Mother):
    pass

child_instance = Child()
print(child_instance.activity())  # Outputs: Working in the office.

In the above example, since Father is listed before Mother in the inheritance list, the activity method of Father takes precedence.

c. Multilevel Inheritance in Python:

Definition:

Multilevel inheritance refers to a scenario where a class is derived from a base class, which is itself derived from another base class. This creates a chain or a hierarchy of classes, where attributes and methods are passed down multiple levels.

How It Works:

In multilevel inheritance, attributes and methods of the topmost base class are inherited by the intermediate base class, which, in turn, might add some more attributes or methods or modify the inherited ones. The derived class at the bottom of this hierarchy will inherit all of these attributes and methods.

Example:

Consider a scenario where we have a base class named Grandfather, an intermediate class Father derived from Grandfather, and a class Child derived from Father.

class Grandfather:
    def ancestry(self):
        return "This is the Grandfather class."

class Father(Grandfather):
    def parenting(self):
        return "This is the Father class, derived from Grandfather."

class Child(Father):
    def identity(self):
        return "This is the Child class, derived from Father."

# Creating an object of the derived class Child
child_instance = Child()

print(child_instance.ancestry())   # Outputs: This is the Grandfather class.
print(child_instance.parenting())  # Outputs: This is the Father class, derived from Grandfather.
print(child_instance.identity())   # Outputs: This is the Child class, derived from Father.

In this example, the Child class, through multilevel inheritance, has access to methods of both its direct parent class (Father) and the grandfather class (Grandfather).

Chain of Inheritance:

In multilevel inheritance, the derived class inherits properties and behaviors in a chained manner. First, the immediate parent class is consulted for attributes and methods, and if not found, the parent class’s parent is consulted, and so on, until the topmost base class.

d. Hierarchical Inheritance in Python:

Definition:

Hierarchical inheritance is a form of inheritance where multiple derived classes inherit attributes and methods from a single base (or parent) class. In this pattern, the base class acts as a foundation, providing common functionalities to multiple child classes.

How It Works:

In hierarchical inheritance, a single base class forms the root from which several derived classes branch out. These derived classes might extend or override the inherited attributes and methods from the base class, but each will have access to the common functionalities defined in the base class.

Example:

Imagine a scenario where we have a base class named Vehicle that provides general attributes and methods for vehicles. From this base class, we derive multiple child classes, like Car, Bike, and Truck, each representing different types of vehicles.

class Vehicle:
    def general_usage(self):
        return "Used as a mode of transportation."

class Car(Vehicle):
    def specifics(self):
        return "A car usually has 4 wheels and can carry 4 to 5 passengers."

class Bike(Vehicle):
    def specifics(self):
        return "A bike has 2 wheels and can carry 1 to 2 passengers."

class Truck(Vehicle):
    def specifics(self):
        return "A truck is primarily used for transporting goods."

# Instantiating the derived classes
car_instance = Car()
bike_instance = Bike()
truck_instance = Truck()

print(car_instance.general_usage())   # Outputs: Used as a mode of transportation.
print(car_instance.specifics())       # Outputs: A car usually has 4 wheels and can carry 4 to 5 passengers.

print(bike_instance.general_usage())  # Outputs: Used as a mode of transportation.
print(bike_instance.specifics())      # Outputs: A bike has 2 wheels and can carry 1 to 2 passengers.

print(truck_instance.general_usage()) # Outputs: Used as a mode of transportation.
print(truck_instance.specifics())     # Outputs: A truck is primarily used for transporting goods.

In the above scenario, Car, Bike, and Truck all inherit the general_usage method from the base Vehicle class but also have their specific methods.

e. Hybrid Inheritance in Python:

Definition:

Hybrid Inheritance is a combination of more than one type of inheritance, meaning it can involve aspects of single, multiple, multilevel, and hierarchical inheritance within a single program. It’s a versatile inheritance pattern that brings together the features of various inheritance models, resulting in a more complex inheritance structure.

How It Works:

In hybrid inheritance, classes are structured in a manner where they inherit attributes and methods from multiple classes, either directly or indirectly. This can create intricate relationships among classes, making the flow of attributes and methods slightly more complex than in simpler inheritance models.

Example:

Consider a scenario where we combine multiple and multilevel inheritance:

# Base classes
class Mother:
    def mother_traits(self):
        return "Mother's traits."

class Father:
    def father_traits(self):
        return "Father's traits."

# Derived class inheriting from both Mother and Father (Multiple Inheritance)
class Child(Mother, Father):
    def child_traits(self):
        return "Child's own traits."

# Further derived class from Child (Multilevel Inheritance)
class GrandChild(Child):
    def grandchild_traits(self):
        return "GrandChild's own traits."

# Creating an object of GrandChild
grandchild_instance = GrandChild()

print(grandchild_instance.mother_traits())       # Outputs: Mother's traits.
print(grandchild_instance.father_traits())       # Outputs: Father's traits.
print(grandchild_instance.child_traits())        # Outputs: Child's own traits.
print(grandchild_instance.grandchild_traits())   # Outputs: GrandChild's own traits.

In the above code, the Child class exhibits multiple inheritance by deriving from both Mother and Father. The GrandChild class then exhibits multilevel inheritance by deriving from the Child. This combination creates a hybrid inheritance pattern.

4. Special Functions in Python Inheritance:

a. super() Function:

Definition:

The super() function in Python is a built-in function that returns a temporary object of the superclass, allowing you to call its methods. It’s predominantly used in the context of inheritance, specifically when a derived class wants to invoke or extend a method from its base class.

Why Use super( ) ?:

When working with classes and inheritance, there might be cases where the derived class needs to extend or modify the behavior of a method defined in its base class rather than completely replacing it. The super() function facilitates this by providing a way to call those base class methods.

How It Works:

The super() function works with the Method Resolution Order (MRO), a mechanism that determines the order in which base classes are accessed. In single inheritance, this is straightforward, but in multiple inheritance scenarios (like with the diamond problem), the MRO ensures methods are called in the correct sequence.

Example:

Imagine a base class Polygon and a derived class Triangle. If we want the derived class to extend the functionality of the base class’s __init__ method, we can use the super() function.

class Polygon:
    def __init__(self, no_of_sides):
        self.n = no_of_sides
        self.sides = [0 for i in range(no_of_sides)]

    def input_sides(self):
        self.sides = [float(input(f"Enter side {i + 1} length: ")) for i in range(self.n)]

class Triangle(Polygon):
    def __init__(self):
        # Calling the __init__ of the base class
        super().__init__(3)  # A triangle always has 3 sides

    def find_area(self):
        a, b, c = self.sides
        # Semi-perimeter
        s = (a + b + c) / 2
        # Area using Heron's formula
        area = (s * (s - a) * (s - b) * (s - c)) ** 0.5
        return area

tri = Triangle()
tri.input_sides()
print(f"Area of the triangle: {tri.find_area()}")

In the above code, the Triangle class’s __init__ method uses super() to call the __init__ method of the Polygon base class, ensuring that the triangle always has 3 sides.

Benefits of super( ) :

  • Code Reusability: Instead of rewriting the entire method, you can reuse parts of the base class method and add only the new functionalities.
  • Maintainability: Using super() makes it easier to maintain the code, especially if changes happen in the base class method in the future.
  • MRO Compliance: In multiple inheritance scenarios, super() ensures that the base class methods are called based on the Method Resolution Order, avoiding potential pitfalls.

b. issubclass():

Definition:

The issubclass() function is a built-in Python function used to determine whether a class is derived from another class, i.e., if one class is a subclass of another.

Syntax:

The syntax of the issubclass() function is:

issubclass(subclass, superclass)

Where:

  • subclass: The class you want to check.
  • superclass: The class you want to check against.

The function will return True if subclass is indeed a subclass of superclass and False otherwise.

Usage:

Suppose you have a class hierarchy with a base class Vehicle and a derived class Car.

class Vehicle:
    pass

class Car(Vehicle):
    pass

# Using issubclass() to check inheritance
print(issubclass(Car, Vehicle))  # Outputs: True
print(issubclass(Vehicle, Car))  # Outputs: False

In this scenario, Car is a subclass of Vehicle, so issubclass(Car, Vehicle) returns True. Conversely, Vehicle is not a subclass of Car, so issubclass(Vehicle, Car) returns False.

Checking Against Multiple Classes:

The issubclass() function can also check if a class is a subclass of any class in a tuple of classes. If the class is a subclass of any class within the tuple, the function returns True.

class Truck:
    pass

print(issubclass(Car, (Vehicle, Truck)))  # Outputs: True

In this example, since Car is a subclass of Vehicle, even though it’s not a subclass of Truck, the function returns True.

c. isinstance():

Definition:

The isinstance() function is a built-in Python function that checks if a given object is an instance of a specific class or any class in a tuple of classes. It helps determine the type or class of an object.

Syntax:

The general syntax of the isinstance() function is:

isinstance(object, classinfo)

Where:

  • object: The object that you want to check.
  • classinfo: A class, type, or a tuple of classes and/or types.

The function returns True if the object is an instance of classinfo (or any class in the tuple) and False otherwise.

Usage:

Let’s consider a simple example:

class Animal:
    pass

class Dog(Animal):
    pass

dog_instance = Dog()

# Using isinstance() to check object's class
print(isinstance(dog_instance, Dog))      # Outputs: True
print(isinstance(dog_instance, Animal))   # Outputs: True
print(isinstance(dog_instance, int))      # Outputs: False

In this scenario, dog_instance is an instance of both the Dog and Animal classes due to inheritance. Thus, isinstance(dog_instance, Dog) and isinstance(dog_instance, Animal) both return True.

Checking Against Multiple Classes:

The isinstance() function can also check if an object is an instance of any class in a tuple of classes.

cat_instance = Animal()

print(isinstance(cat_instance, (Dog, Animal)))   # Outputs: True

Even though cat_instance is not an instance of Dog, it is an instance of Animal. Hence, isinstance(cat_instance, (Dog, Animal)) returns True.

5. Advantages of Inheritance in Python:

  • Code Reusability: Avoids the repetition of code, as child classes can reuse methods and attributes from parent classes.
  • Extensibility: Enables modification of existing code through derived classes.
  • Logical Structure: Provides a clear hierarchical structure that’s intuitive and organized.
  • Transitive Nature: If class B inherits from class A, and class C inherits from class B, class C will inherit A’s properties too.

6. Disadvantages of Inheritance in Python:

  • Overhead: Too much inheritance can increase complexity, leading to difficulties in understanding and debugging.
  • Dependency: Child classes are dependent on parent classes, meaning changes in the parent class can inadvertently affect child classes.
  • Loss of Flexibility: The structure established by inheritance can sometimes be too rigid, especially in cases of deep inheritance hierarchies or complex hybrid inheritance scenarios. This could make future changes or extensions to the system challenging.
  • Hidden Dependencies: Deep inheritance can introduce hidden dependencies in the system, making some modifications have unforeseen consequences.
  • Overhead in Initialization: If constructors (initialization methods) in parent classes are heavy or complex, every time a derived class object is created, it might carry an unnecessary overhead, especially if the derived class doesn’t need the parent’s detailed initialization.

7. Conclusion:

Inheritance, one of the four pillars of Object-Oriented Programming, provides a powerful mechanism for code reusability, structure, and organization in Python. It allows for the definition of a new class based on an existing class, thereby inheriting its attributes and behaviors. This hierarchical relationship introduces a natural and intuitive structure to the code.

However, like any powerful tool, inheritance should be used judiciously. Deep and complex inheritance hierarchies can introduce challenges that might outweigh the benefits. Understanding the nuances, advantages, and potential pitfalls of inheritance ensures that developers can harness its capabilities wisely, crafting code that is both efficient and maintainable.

Leave a Reply