Python - Classes

17.
Discuss the concept of multiple inheritance in Python.

In Python, multiple inheritance is a feature that allows a class to inherit attributes and methods from more than one base class. This allows the derived class to have a combination of features from multiple classes, promoting code reuse and flexibility.

Let's demonstrate the concept of multiple inheritance with an example:

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

class Mammal:
    def give_birth(self):
        return "Giving birth to live young"

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

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

# Accessing methods from multiple base classes
print(dog.speak())
print(dog.give_birth())
print(dog.bark())

In this example, the Animal class has a speak method, the Mammal class has a give_birth method, and the Dog class is derived from both Animal and Mammal. The Dog class can access methods from both base classes, demonstrating multiple inheritance.

Output:

Some generic sound
Giving birth to live young
Woof!

18.
How do you use the isinstance() and issubclass() functions in Python?

In Python, the isinstance() function is used to check if an object is an instance of a particular class or a tuple of classes. The issubclass() function is used to check if a class is a subclass of another class or a tuple of classes.

Let's illustrate the use of isinstance() and issubclass() with an example:

class Animal:
    pass

class Mammal(Animal):
    pass

class Dog(Mammal):
    pass

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

# Using isinstance to check if an object is an instance of a class
is_dog_instance = isinstance(dog_instance, Dog)
is_mammal_instance = isinstance(dog_instance, Mammal)
is_animal_instance = isinstance(dog_instance, Animal)

# Using issubclass to check if a class is a subclass of another class
is_dog_subclass_of_mammal = issubclass(Dog, Mammal)
is_mammal_subclass_of_animal = issubclass(Mammal, Animal)
is_dog_subclass_of_animal = issubclass(Dog, Animal)

# Displaying the results
print(f"Is Dog an instance of Dog class? {is_dog_instance}")
print(f"Is Dog an instance of Mammal class? {is_mammal_instance}")
print(f"Is Dog an instance of Animal class? {is_animal_instance}")
print()
print(f"Is Dog a subclass of Mammal class? {is_dog_subclass_of_mammal}")
print(f"Is Mammal a subclass of Animal class? {is_mammal_subclass_of_animal}")
print(f"Is Dog a subclass of Animal class? {is_dog_subclass_of_animal}")

In this example, we have three classes (Animal, Mammal, and Dog). We use isinstance() to check if an object is an instance of a particular class, and issubclass() to check if a class is a subclass of another class. The results are then displayed.

Output:

Is Dog an instance of Dog class? True
Is Dog an instance of Mammal class? True
Is Dog an instance of Animal class? True

Is Dog a subclass of Mammal class? True
Is Mammal a subclass of Animal class? True
Is Dog a subclass of Animal class? True

19.
What is the purpose of abstract classes and methods in Python?

In Python, abstract classes and methods are part of the abstract base classes (ABCs) module and provide a way to define a common interface for a group of related classes. An abstract class is a class that cannot be instantiated, and an abstract method is a method that must be implemented by any concrete (non-abstract) subclass. Abstract classes and methods help enforce a common structure among different classes.

Let's demonstrate the purpose of abstract classes and methods with an example:

from abc import ABC, abstractmethod

# Abstract class with abstract method
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# Concrete class implementing the abstract method
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

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

# Concrete class implementing the abstract method
class Square(Shape):
    def __init__(self, side):
        self.side = side

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

# Attempting to create an instance of the abstract class will result in a TypeError
# shape = Shape()

# Creating objects of concrete classes
circle = Circle(5)
square = Square(4)

# Calling the area method on concrete objects
circle_area = circle.area()
square_area = square.area()

# Displaying the results
print(f"Area of the circle: {circle_area}")
print(f"Area of the square: {square_area}")

In this example, the Shape class is an abstract class with an abstract method area. The Circle and Square classes are concrete classes that inherit from Shape and implement the area method. Attempting to create an instance of the abstract class will result in a TypeError.

Output:

Area of the circle: 78.5
Area of the square: 16

20.
Explain the difference between composition and inheritance.

In object-oriented programming, composition and inheritance are two ways to achieve code reuse and design relationships between classes. The key difference lies in how they establish relationships between classes and promote code reuse.

Composition:

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self):
        # Composition: Car has an Engine
        self.engine = Engine()

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

# Creating an object of the Car class
car = Car()

# Using composition to access functionalities of the Engine class
result = car.drive()

# Displaying the result
print(result)

In composition, a class is composed of other classes as parts. In the example, the Car class has an instance of the Engine class. The Car class can access and use the functionalities of the Engine class through composition.

Output:

Car is moving. Engine started

Inheritance:

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

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

# Creating an object of the Dog class
dog = Dog()

# Using inheritance to access functionalities of the Animal class
result = dog.make_sound()

# Displaying the result
print(result)

In inheritance, a class inherits attributes and methods from another class. In the example, the Dog class inherits from the Animal class. The Dog class can access and use the functionalities of the Animal class through inheritance.

Output:

Some generic sound

Comparison:

Composition allows for more flexibility and is often favored when designing relationships between classes, as it avoids some issues related to tight coupling. Inheritance promotes code reuse but can lead to a rigid class hierarchy, and changes in the base class can affect derived classes.