Python - Inheritance

21.
What is the role of abstract base classes (ABCs) in Python inheritance?

Abstract Base Classes (ABCs) in Python are used to define abstract methods and ensure that specific methods are implemented in the subclasses. They provide a way to create a common interface for a group of related classes.

Let's consider an example to illustrate the role of Abstract Base Classes in Python inheritance:

from abc import ABC, abstractmethod

# Define an abstract base class (ABC)
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

# Create a concrete class that inherits from the Shape ABC
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14 * self.radius

# Create another concrete class that inherits from the Shape ABC
class Square(Shape):
    def __init__(self, side_length):
        self.side_length = side_length

    def area(self):
        return self.side_length ** 2

    def perimeter(self):
        return 4 * self.side_length

# Attempt to create an instance of the abstract class (Shape)
try:
    shape_instance = Shape()  # This should raise a TypeError
except TypeError as e:
    error_message = f"TypeError: {e}"

# Create instances of the concrete classes (Circle and Square)
circle_instance = Circle(radius=5)
square_instance = Square(side_length=4)

# Use the methods defined in the Shape ABC
circle_area = circle_instance.area()
circle_perimeter = circle_instance.perimeter()

square_area = square_instance.area()
square_perimeter = square_instance.perimeter()

print(error_message)       # Output: TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
print(circle_area)         # Output: 78.5
print(circle_perimeter)    # Output: 31.400000000000002
print(square_area)         # Output: 16
print(square_perimeter)    # Output: 16

In this example, the Shape class is an abstract base class (ABC) that defines two abstract methods: area and perimeter. The concrete classes Circle and Square inherit from Shape and provide implementations for these abstract methods.

The program demonstrates how ABCs help enforce a specific interface for related classes, ensuring that subclasses implement the required methods.

Output:

TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
78.5
31.400000000000002
16
16

22.
How can you implement interfaces using inheritance in Python?

In Python, interfaces are not explicitly defined, but you can implement a similar concept using inheritance and abstract methods. Abstract methods in abstract base classes (ABCs) can serve as a way to define the interface that subclasses must adhere to.

Let's consider an example to illustrate how to implement interfaces using inheritance in Python:

from abc import ABC, abstractmethod

# Define an interface using an abstract base class (ABC)
class ShapeInterface(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

# Implement the interface in a concrete class
class Circle(ShapeInterface):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14 * self.radius

# Create an instance of the concrete class
circle_instance = Circle(radius=5)

# Use the methods defined in the interface
circle_area = circle_instance.area()
circle_perimeter = circle_instance.perimeter()

print(circle_area)         # Output: 78.5
print(circle_perimeter)    # Output: 31.400000000000002

In this example, the ShapeInterface class serves as an interface with two abstract methods: area and perimeter. The Circle class inherits from ShapeInterface and provides implementations for these abstract methods.

The program demonstrates how to implement interfaces using inheritance in Python. The abstract methods in the interface enforce a specific structure that subclasses must adhere to.

Output:

78.5
31.400000000000002

23.
Discuss the use of composition as an alternative to inheritance.

Composition is an object-oriented design principle where objects are composed of other objects, allowing for greater flexibility and code reuse. It promotes building objects by combining simpler, smaller objects rather than relying on inheritance hierarchies.

Let's consider an example to illustrate the use of composition as an alternative to inheritance in Python:

# Define a simple class representing a Engine
class Engine:
    def start(self):
        return "Engine started"

# Define another class representing a Car that uses composition
class Car:
    def __init__(self):
        # Composition: Car has an Engine
        self.engine = Engine()

    def drive(self):
        return f"Car is driving. {self.engine.start()}"

# Create an instance of the Car class
car_instance = Car()

# Use the methods defined in the composed objects
drive_result = car_instance.drive()

print(drive_result)  # Output: Car is driving. Engine started

In this example, the Car class does not inherit from the Engine class. Instead, it contains an instance of the Engine class as an attribute. This is an example of composition, where a complex object (Car) is built by combining simpler objects (Engine).

The program demonstrates how composition allows for code reuse and flexibility by assembling objects at runtime rather than relying on fixed inheritance relationships.

Output:

Car is driving. Engine started

24.
What are the advantages of using inheritance in software design?

Inheritance is a key feature of object-oriented programming that allows a new class to inherit attributes and behaviors from an existing class. This promotes code reuse, extensibility, and polymorphism.

Let's consider an example to illustrate the advantages of using inheritance in software design:

# Define a base class representing a Shape
class Shape:
    def __init__(self, color):
        self.color = color

    def describe(self):
        return f"A {self.color} shape"

# Define a derived class representing a Circle
class Circle(Shape):
    def __init__(self, color, radius):
        # Call the constructor of the base class using super()
        super().__init__(color)
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

# Define another derived class representing a Square
class Square(Shape):
    def __init__(self, color, side_length):
        # Call the constructor of the base class using super()
        super().__init__(color)
        self.side_length = side_length

    def area(self):
        return self.side_length ** 2

# Create instances of the derived classes
circle_instance = Circle(color="red", radius=5)
square_instance = Square(color="blue", side_length=4)

# Use the methods and attributes from the base class and derived classes
circle_description = circle_instance.describe()
circle_area = circle_instance.area()

square_description = square_instance.describe()
square_area = square_instance.area()

print(circle_description)  # Output: A red shape
print(circle_area)         # Output: 78.5

print(square_description)  # Output: A blue shape
print(square_area)         # Output: 16

In this example, the Shape class serves as a base class with a method describe. The derived classes Circle and Square inherit from Shape and extend its functionality by adding methods area. Instances of the derived classes can use both the methods and attributes of the base class and their own additional methods.

The program demonstrates how inheritance facilitates code reuse and extensibility, allowing the creation of specialized classes that inherit and build upon the functionality of a more general base class.

Output:

A red shape
78.5
A blue shape
16