# Polymorphism &#x20;

Polymorphism is a core concept in object-oriented programming (OOP) that refers to the ability of different objects to be used interchangeably, even if they have different implementations. In other words, polymorphism allows objects of different classes to be treated as if they were objects of the same class, as long as they share a common interface or parent class.

Polymorphism can be implemented in different ways, such as through inheritance, duck typing, or operator overloading. In inheritance-based polymorphism, subclasses inherit methods and attributes from their parent class, and can override or extend them as needed. In duck typing-based polymorphism, objects are evaluated based on their behavior rather than their class or type. And in operator overloading-based polymorphism, objects can define their own behavior for built-in operators like +, -, \*, and /.

Polymorphism is a powerful and flexible feature of OOP that allows for more modular, reusable, and extensible code. It can simplify code by abstracting away implementation details and allowing different objects to be used interchangeably, which can improve maintainability and flexibility.&#x20;

## Types of Polymorphism&#x20;

There are several types of polymorphism in object-oriented programming, including:

1. Inheritance-based Polymorphism: Inheritance allows subclasses to inherit methods and attributes from their parent class, and override or extend them as needed. This allows objects of different classes to be treated as if they were objects of the same class, as long as they share a common interface or parent class.
2. Duck Typing-based Polymorphism: Duck typing is a programming concept that evaluates objects based on their behavior rather than their class or type. This means that any object that has the necessary methods or attributes can be used interchangeably, even if it's not of the same class or type.
3. Operator Overloading-based Polymorphism: Operator overloading allows objects to define their own behavior for built-in operators like +, -, \*, and /, which allows objects of different classes to be used interchangeably for arithmetic operations.
4. Interface-based Polymorphism: Interfaces define a set of methods or attributes that a class must implement, without specifying how they should be implemented. This allows objects of different classes to be used interchangeably as long as they implement the same interface.
5. Parametric Polymorphism: Parametric polymorphism allows functions or classes to be written in a generic way, without specifying a specific type. This allows the same code to be used for different types of objects, as long as they meet certain requirements.

Each type of polymorphism has its own strengths and weaknesses, and the choice of which one to use depends on the specific requirements of the program.&#x20;

## Inheritance-based Polymorphism&#x20;

Inheritance-based polymorphism is a type of polymorphism that allows objects of different classes to be used interchangeably as long as they share a common parent class or interface. Inheritance is a mechanism in object-oriented programming that allows one class to inherit properties and methods from another class, which can then be overridden or extended as needed.

Here's an example of how inheritance-based polymorphism works in Python:

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")

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

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

class Bird(Animal):
    def speak(self):
        return "Tweet"

animals = [Dog("Fido"), Cat("Mittens"), Bird("Tweety")]

for animal in animals:
    print(animal.name + " says " + animal.speak())
```

In this example, we have a parent class called `Animal` that defines an abstract method called `speak()`, which raises a `NotImplementedError` exception. This method must be implemented by any subclasses that inherit from `Animal`.

We then define three subclasses of `Animal`: `Dog`, `Cat`, and `Bird`, each of which provides its own implementation of `speak()`.

Finally, we create a list of `Animal` objects, which includes instances of `Dog`, `Cat`, and `Bird`. We can then loop over the list and call the `speak()` method on each object, which will call the appropriate implementation of `speak()` based on the object's class.

This example demonstrates how inheritance-based polymorphism allows us to treat objects of different classes as if they were objects of the same class, as long as they share a common parent class or interface. In this case, all of the `Animal` subclasses share the `speak()` method defined in the `Animal` class, which allows us to call `speak()` on any `Animal` object without knowing its specific subclass.&#x20;

### a. Method Overriding&#x20;

Method overriding is a form of inheritance-based polymorphism that allows a subclass to provide a different implementation of a method that is already defined in its parent class. When a method is overridden in a subclass, the subclass version of the method is used instead of the parent class version when the method is called on an object of the subclass.

Here's an example of method overriding in Python:

```python
class Animal:
    def make_sound(self):
        print("The animal makes a sound")

class Dog(Animal):
    def make_sound(self):
        print("The dog barks")

class Cat(Animal):
    def make_sound(self):
        print("The cat meows")

a = Animal()
a.make_sound()

d = Dog()
d.make_sound()

c = Cat()
c.make_sound()
```

In this example, we have a parent class called `Animal` with a method called `make_sound()`, which prints a generic message. We then define two subclasses of `Animal`: `Dog` and `Cat`, each of which overrides the `make_sound()` method with its own implementation.

When we create an instance of `Animal` and call `make_sound()`, the parent class implementation of the method is used, which prints "The animal makes a sound". However, when we create instances of `Dog` and `Cat` and call `make_sound()`, the overridden implementation of the method in the appropriate subclass is used instead.

This example demonstrates how method overriding allows a subclass to provide a different implementation of a method that is already defined in its parent class. When the method is called on an object of the subclass, the overridden version of the method is used instead of the parent class version.&#x20;

### b. Polymorphic Methods&#x20;

Polymorphic methods are methods that can take objects of different classes and behave differently depending on the type of the object that is passed in. Polymorphic methods are a form of inheritance-based polymorphism and are often used in combination with method overriding.

Here's an example of a polymorphic method in Python:

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def make_sound(self):
        print("The animal makes a sound")

class Dog(Animal):
    def make_sound(self):
        print("The dog barks")

class Cat(Animal):
    def make_sound(self):
        print("The cat meows")

def make_animal_sound(animal):
    animal.make_sound()

a = Animal("Generic animal")
d = Dog("Fido")
c = Cat("Whiskers")

make_animal_sound(a) # prints "The animal makes a sound"
make_animal_sound(d) # prints "The dog barks"
make_animal_sound(c) # prints "The cat meows"
```

In this example, we define a polymorphic method called `make_animal_sound()`, which takes an object of type `Animal` or one of its subclasses and calls the `make_sound()` method on it. Because the `make_sound()` method is overridden in the `Dog` and `Cat` subclasses, the appropriate implementation of the method is called depending on the type of the object that is passed in.

When we call `make_animal_sound()` with an instance of `Animal`, the parent class implementation of `make_sound()` is called, which prints "The animal makes a sound". When we call `make_animal_sound()` with an instance of `Dog`, the `Dog` subclass implementation of `make_sound()` is called, which prints "The dog barks". Similarly, when we call `make_animal_sound()` with an instance of `Cat`, the `Cat` subclass implementation of `make_sound()` is called, which prints "The cat meows".

This example demonstrates how polymorphic methods allow us to write code that can accept objects of different classes and behave differently depending on the type of the object that is passed in. Polymorphic methods are a powerful tool in object-oriented programming that can help make our code more flexible and reusable.

### c. Abstract Base Classes&#x20;

Abstract Base Classes (ABCs) are a form of inheritance-based polymorphism that allow us to define abstract methods in a parent class that must be implemented in its subclasses. ABCs are defined using the `abc` module in Python and are useful when we want to enforce certain behaviors or properties in subclasses.

Here's an example of an ABC in Python:

```python
import abc

class Shape(abc.ABC):
    @abc.abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

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

r = Rectangle(5, 10)
print(r.area()) # prints 50

c = Circle(3)
print(c.area()) # prints 28.26
```

In this example, we define an abstract class called `Shape` using the `abc` module. The `Shape` class has one abstract method called `area()`, which must be implemented in any subclasses of `Shape`. We then define two subclasses of `Shape`: `Rectangle` and `Circle`, both of which implement the `area()` method with their own calculations.

When we create an instance of `Rectangle` or `Circle` and call the `area()` method, the appropriate implementation of the method in the subclass is called. Because `Rectangle` and `Circle` both inherit from `Shape`, they must implement the `area()` method, ensuring that any instances of these subclasses will have a `area()` method defined.

This example demonstrates how ABCs allow us to define abstract methods in a parent class that must be implemented in its subclasses. ABCs are useful when we want to enforce certain behaviors or properties in subclasses, ensuring that any instances of the subclasses will have the required methods or attributes.

## Duck Typing-based Polymorphism&#x20;

Duck typing-based polymorphism is a form of polymorphism in Python that is based on the principle of "if it walks like a duck and quacks like a duck, then it must be a duck". This means that if an object has the necessary attributes or methods that are expected by a function or method, then that object can be used in place of another object that has the same attributes or methods.

Here's an example of duck typing-based polymorphism in Python:

```python
class Car:
    def drive(self):
        print("The car is driving")

class Bicycle:
    def ride(self):
        print("The bicycle is riding")

def take_vehicle(vehicle):
    vehicle.drive()

car = Car()
bike = Bicycle()

take_vehicle(car) # prints "The car is driving"
take_vehicle(bike) # raises AttributeError: 'Bicycle' object has no attribute 'drive'
```

In this example, we define two classes: `Car` and `Bicycle`. We then define a function called `take_vehicle()` that takes a `vehicle` object and calls its `drive()` method. When we call `take_vehicle()` with an instance of `Car`, the `drive()` method of the `Car` object is called and the message "The car is driving" is printed. However, when we call `take_vehicle()` with an instance of `Bicycle`, an `AttributeError` is raised because `Bicycle` objects don't have a `drive()` method.

This example demonstrates how duck typing-based polymorphism works in Python. The `take_vehicle()` function doesn't care about the specific type of the `vehicle` object that is passed in, as long as it has a `drive()` method. This allows us to write code that is more flexible and doesn't rely on specific class hierarchies or inheritance relationships.

Duck typing-based polymorphism is often used in Python because it allows for more dynamic and flexible code. However, it's important to be aware of the potential for errors or unexpected behavior when using duck typing, as it relies on objects having the expected attributes and methods, and doesn't enforce any strict type checking.

## Operator Overloading-based Polymorphism&#x20;

Operator overloading-based polymorphism is a form of polymorphism in Python that allows us to define how operators (such as `+`, `-`, `*`, `/`, etc.) behave for objects of our own custom classes. This allows us to write code that uses the same operators for different types of objects, as long as those objects have the necessary methods defined.

Here's an example of operator overloading-based polymorphism in Python:

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):
        return f"({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)

v3 = v1 + v2
print(v3) # prints "(4, 6)"

v4 = v2 - v1
print(v4) # prints "(2, 2)"

v5 = v1 * 2
print(v5) # prints "(2, 4)"
```

In this example, we define a class called `Vector` that represents a 2D vector. We overload the `+`, `-`, and `*` operators using special methods (`__add__()`, `__sub__()`, and `__mul__()`, respectively) to define how these operators should behave for `Vector` objects. We also define a `__str__()` method to allow us to print `Vector` objects in a more human-readable format.

When we create instances of `Vector` and use the overloaded operators, the appropriate implementation of the operator is called based on the types of the operands. This allows us to write code that uses the same operators for different types of objects, as long as those objects have the necessary methods defined.

Operator overloading-based polymorphism is a powerful feature of Python that allows us to write code that is more concise and expressive. However, it's important to use operator overloading judiciously and to make sure that the behavior of overloaded operators is consistent with what users would expect.

## Polymorphism with Multiple Inheritance&#x20;

Polymorphism with multiple inheritance is a concept in object-oriented programming that allows a class to inherit from multiple parent classes and exhibit polymorphic behavior based on the methods defined in each of its parent classes. This means that the same method name can have different implementations in different parent classes, and the subclass can choose which implementation to use based on the context in which the method is called.

Let's take an example to illustrate this concept:

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass

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

class Cat(Animal):
    def speak(self):
        return "Meow!"

class DogCat(Dog, Cat):
    pass

d = DogCat("Fido")
print(d.speak()) # prints "Woof!"
```

In this example, we define three classes: `Animal`, `Dog`, and `Cat`. `Dog` and `Cat` are both subclasses of `Animal`, and they both define a `speak()` method with different implementations. We also define a third class called `DogCat`, which inherits from both `Dog` and `Cat`.

When we create an instance of `DogCat` and call its `speak()` method, Python looks for the `speak()` method in `Dog` first, since it is the first parent class listed in the definition of `DogCat`. Since `Dog` defines a `speak()` method, that implementation is used, and the method returns "Woof!".

If we were to reverse the order of the parent classes in the definition of `DogCat`, so that `Cat` comes first, then calling `speak()` on an instance of `DogCat` would return "Meow!" instead of "Woof!".

Polymorphism with multiple inheritance can be a powerful tool for creating complex object hierarchies and building code that is flexible and extensible. However, it's important to use multiple inheritance carefully, as it can also make code harder to read and understand if not used judiciously.&#x20;

## Function Polymorphism in Python&#x20;

Function polymorphism in Python is the ability of a function to accept arguments of different types and behave differently based on the type of argument passed. This is similar to polymorphism in object-oriented programming, but instead of classes and objects, we are working with functions and arguments.

One common example of function polymorphism in Python is the built-in `len()` function. The `len()` function can accept different types of arguments, including strings, lists, tuples, and dictionaries, and it will return the length of the argument passed based on its type.

Here's an example of using the `len()` function with different types of arguments:

```python
s = "Hello, world!"
l = [1, 2, 3, 4, 5]
t = (6, 7, 8, 9, 10)
d = {"a": 1, "b": 2, "c": 3}

print(len(s)) # prints 13
print(len(l)) # prints 5
print(len(t)) # prints 5
print(len(d)) # prints 3
```

In this example, we pass different types of arguments to the `len()` function, and it behaves differently based on the type of argument passed. When we pass a string, `len()` returns the number of characters in the string. When we pass a list or tuple, `len()` returns the number of elements in the container. When we pass a dictionary, `len()` returns the number of key-value pairs in the dictionary.

We can also create our own functions that exhibit polymorphic behavior based on the types of their arguments. For example, we can define a function called `add()` that adds two numbers or concatenates two strings, depending on the types of the arguments passed:

```python
def add(x, y):
    if type(x) == type(y):
        return x + y
    else:
        return str(x) + str(y)

print(add(1, 2)) # prints 3
print(add("hello", "world")) # prints "helloworld"
print(add(3, "dogs")) # prints "3dogs"
```

In this example, the `add()` function checks the types of the arguments passed and behaves differently based on whether they are the same type or not. If the arguments are the same type, the function adds them together. If they are different types, the function converts them to strings and concatenates them.

Function polymorphism can make our code more flexible and adaptable to different types of data, and it can also make our code more concise and easier to read.&#x20;

## Class Polymorphism in Python  <a href="#class-poly" id="class-poly"></a>

Class polymorphism in Python is the ability of a class to take on different forms or behaviors depending on its context. This is achieved through inheritance, method overriding, and the use of abstract classes and interfaces.

Here's an example of class polymorphism in Python using inheritance:

```python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass

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

class Cat(Animal):
    def speak(self):
        return "Meow!"

class Bird(Animal):
    def speak(self):
        return "Chirp!"

animals = [Dog("Rufus"), Cat("Whiskers"), Bird("Tweety")]

for animal in animals:
    print(animal.name + ": " + animal.speak())
```

In this example, we define a base class called `Animal` with an abstract method called `speak()`. We then define three subclasses (`Dog`, `Cat`, and `Bird`) that inherit from the `Animal` class and implement the `speak()` method in their own way. Finally, we create a list of `Animal` objects containing instances of each subclass, and we call the `speak()` method on each object.

The result of running this code will be:

```python
Rufus: Woof!
Whiskers: Meow!
Tweety: Chirp!
```

As we can see, each `Animal` object behaves differently based on its type. This is an example of class polymorphism.

We can also achieve class polymorphism in Python through method overriding. Method overriding is the process of redefining a method in a subclass that already exists in its parent class. Here's an example:

```python
class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

shapes = [Rectangle(5, 10), Circle(7)]

for shape in shapes:
    print(shape.area())
```

In this example, we define a base class called `Shape` with an abstract method called `area()`. We then define two subclasses (`Rectangle` and `Circle`) that inherit from the `Shape` class and override the `area()` method to calculate the area of a rectangle or a circle, respectively. Finally, we create a list of `Shape` objects containing instances of each subclass, and we call the `area()` method on each object.

The result of running this code will be:

```python
50
153.86
```

As we can see, each `Shape` object behaves differently based on its type. This is another example of class polymorphism achieved through method overriding.

## Polymorphism and Inheritance&#x20;

Polymorphism and inheritance are closely related concepts in object-oriented programming. Inheritance allows a subclass to inherit properties and methods from its superclass, while polymorphism allows objects of different classes to be treated as if they were of the same class.

Inheritance is the process by which a class can inherit properties and methods from its parent class. When a class inherits from another class, it automatically gets all the methods and properties of the parent class. This allows us to create new classes that are based on existing classes and to reuse code that has already been written. Inheritance is a key feature of object-oriented programming, and it enables us to create a hierarchy of related classes.

Polymorphism, on the other hand, is the ability of objects of different classes to be treated as if they were of the same class. This means that we can write code that works with objects of a certain type, and that same code will work with objects of other types as long as they have the same interface. Polymorphism allows us to write more generic code that can work with a wider range of objects, and it makes our code more flexible and adaptable.

One of the ways in which polymorphism is achieved in object-oriented programming is through inheritance. By inheriting from a parent class, a subclass can use the methods and properties of the parent class, and it can also add its own methods and properties. This means that an object of the subclass can be treated as if it were an object of the parent class, which enables polymorphic behavior.

For example, consider the following code:

```python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass

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

class Cat(Animal):
    def speak(self):
        return "Meow!"

class Bird(Animal):
    def speak(self):
        return "Chirp!"

def animal_speak(animal):
    print(animal.name + ": " + animal.speak())

dog = Dog("Rufus")
cat = Cat("Whiskers")
bird = Bird("Tweety")

animal_speak(dog)
animal_speak(cat)
animal_speak(bird)
```

In this example, we define a base class called `Animal` with an abstract method called `speak()`. We then define three subclasses (`Dog`, `Cat`, and `Bird`) that inherit from the `Animal` class and implement the `speak()` method in their own way. Finally, we define a function called `animal_speak()` that takes an `Animal` object as an argument and calls its `speak()` method.

When we create objects of the `Dog`, `Cat`, and `Bird` classes, we can pass them to the `animal_speak()` function, which treats them as if they were `Animal` objects. This is possible because the `Dog`, `Cat`, and `Bird` classes inherit from the `Animal` class and implement the `speak()` method in a polymorphic way. This enables us to write more generic code that works with a wider range of objects.

\ <br>
