Python - Polymorphism

5.
What is the purpose of the * and ** syntax in method overloading?

In Python, the * (single asterisk) and ** (double asterisk) syntax is used to define variable-length argument lists in function definitions. These are commonly referred to as *args (for variable-length positional arguments) and **kwargs (for variable-length keyword arguments).

Purpose of *args and **kwargs:

The purpose of *args and **kwargs in method overloading is to allow functions to accept a variable number of arguments. This flexibility is especially useful when you want to define functions that can handle different numbers of parameters, providing more versatility and making the code more readable and concise.

Example Program:

def add_numbers(*args):
    total = 0
    for num in args:
        total += num
    return total

def concatenate_strings(**kwargs):
    result = ""
    for key, value in kwargs.items():
        result += f"{key}: {value} "
    return result.strip()

# Example using *args
sum_result = add_numbers(2, 3, 4, 5)

# Example using **kwargs
concat_result = concatenate_strings(first_name="John", last_name="Doe", age=30)

print(sum_result)
print(concat_result)

Output:

14
first_name: John last_name: Doe age: 30

In this example, the add_numbers function uses *args to accept a variable number of positional arguments and calculates their sum. The concatenate_strings function uses **kwargs to accept a variable number of keyword arguments and concatenates them into a string. These functions can be called with different numbers of arguments, showcasing the flexibility provided by *args and **kwargs.

Using these variable-length argument lists can simplify function definitions and make the code more adaptable to different use cases.


6.
Explain the concept of method overriding in Python.

Method overriding is a concept in object-oriented programming (OOP) where a subclass provides a specific implementation for a method that is already defined in its superclass. This allows the subclass to provide its own version of the method, giving it a chance to customize or extend the behavior inherited from the superclass.

Example Program:

class Animal:
    def speak(self):
        return "Animal speaks"

class Dog(Animal):
    def speak(self):
        return "Dog barks"

class Cat(Animal):
    def speak(self):
        return "Cat meows"

# Example of Method Overriding
def animal_speak(animal):
    return animal.speak()

dog = Dog()
cat = Cat()

print(animal_speak(dog))
print(animal_speak(cat))

Output:

Dog barks
Cat meows

In this example, we have a base class Animal with a method speak. The Dog and Cat classes are subclasses of Animal and both override the speak method with their own implementations.

The animal_speak function takes an object of type Animal and calls its speak method. The actual implementation of the speak method is determined at runtime based on the type of the object passed to the function. This demonstrates the concept of method overriding in Python.

Method overriding allows for polymorphic behavior, where objects of different types (subclasses) can be treated as objects of the same type (superclass) while executing the same method.


7.
How can you achieve polymorphism using function overloading?

Python does not support traditional function overloading like some other languages, but you can achieve a form of polymorphism using default values for function parameters. By providing default values, a function can be called with different numbers of arguments, resulting in polymorphic behavior.

Example Program:

def add_numbers(a, b=0, c=0):
    return a + b + c

# Example of Polymorphism using Function Overloading
result1 = add_numbers(2, 3)
result2 = add_numbers(2, 3, 4)

print(result1)
print(result2)

Output:

5
9

In this example, the add_numbers function is designed to take three parameters, but the second and third parameters have default values of 0. This allows the function to be called with different numbers of arguments. When called with two arguments, the function uses the default value for the third parameter, and when called with three arguments, it uses the provided values for all parameters.

While this approach does not provide the same level of flexibility as true function overloading in some other languages, it does allow for a level of polymorphism in Python by accommodating different numbers of arguments in a function call.


8.
Discuss the role of abstract classes and methods in polymorphism.

Abstract classes and methods play a crucial role in achieving polymorphism and abstraction in object-oriented programming (OOP). An abstract class is a class that cannot be instantiated and is meant to be subclassed. Abstract methods, defined in an abstract class, are methods that must be implemented by any concrete (non-abstract) subclass. This encourages a consistent interface among different classes, contributing to polymorphic behavior.

Example Program:

from abc import ABC, abstractmethod

# Abstract Class with Abstract Method
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# Concrete Subclass implementing Abstract Method
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

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

# Concrete Subclass implementing Abstract Method
class Square(Shape):
    def __init__(self, side):
        self.side = side

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

# Function demonstrating polymorphism with abstract classes
def print_area(shape):
    return shape.area()

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

# Using polymorphism with abstract classes
area1 = print_area(circle)
area2 = print_area(square)

print(area1)
print(area2)

Output:

78.5
16

In this example, the Shape class is an abstract class with an abstract method area. The Circle and Square classes are concrete subclasses of Shape and implement the area method.

The print_area function demonstrates polymorphism by accepting any object of type Shape or its subclasses. This allows the function to work with different types of shapes without knowing their specific implementations, showcasing the power of abstract classes and methods in achieving polymorphism and abstraction.