# 196. Polymorphism in OOP

Polymorphism is a core concept in object-oriented programming (OOP) that allows objects of different classes to be treated as instances of the same class through a common interface. In Python, polymorphism can be implemented using method overriding, allowing subclasses to provide their own implementations of methods defined in a parent class.

Here are 10 Python code snippets demonstrating the implementation of polymorphism through method overriding and related concepts.

***

**1. Basic Polymorphism Example**

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

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

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

# Polymorphism in action
def animal_speak(animal: Animal):
    print(animal.speak())

dog = Dog()
cat = Cat()

animal_speak(dog)  # Outputs: Woof
animal_speak(cat)  # Outputs: Meow
```

***

**2. Polymorphism with Multiple Inheritance**

```python
class Bird:
    def fly(self):
        return "Flying in the sky"

class Fish:
    def swim(self):
        return "Swimming in the ocean"

class Duck(Bird, Fish):
    def sound(self):
        return "Quack"

# Using polymorphism to call methods from different parent classes
duck = Duck()
print(duck.fly())  # Outputs: Flying in the sky
print(duck.swim())  # Outputs: Swimming in the ocean
print(duck.sound())  # Outputs: Quack
```

***

**3. Polymorphism with Abstraction (Abstract Base Class)**

```python
from abc import ABC, abstractmethod

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

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

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

class Square(Shape):
    def __init__(self, side):
        self.side = side

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

# Demonstrating polymorphism
def print_area(shape: Shape):
    print(f"Area: {shape.area()}")

circle = Circle(5)
square = Square(4)

print_area(circle)  # Outputs: Area: 78.5
print_area(square)  # Outputs: Area: 16
```

***

**4. Polymorphism with Method Resolution Order (MRO)**

```python
class A:
    def method(self):
        print("Method from class A")

class B(A):
    def method(self):
        print("Method from class B")

class C(B):
    def method(self):
        print("Method from class C")

# Demonstrating method resolution order (MRO)
c = C()
c.method()  # Outputs: Method from class C
```

***

**5. Polymorphism with Dynamic Method Overriding**

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

    def work(self):
        return f"{self.name} is working."

class Manager(Employee):
    def work(self):
        return f"{self.name} is managing the team."

class Developer(Employee):
    def work(self):
        return f"{self.name} is writing code."

# Polymorphism with dynamic method overriding
employees = [Manager("Alice"), Developer("Bob")]

for employee in employees:
    print(employee.work())  # Outputs: Alice is managing the team. Bob is writing code.
```

***

**6. Polymorphism in a Real-World Example (Payment System)**

```python
class PaymentMethod:
    def process_payment(self, amount):
        pass

class CreditCard(PaymentMethod):
    def process_payment(self, amount):
        return f"Processed payment of {amount} using Credit Card"

class PayPal(PaymentMethod):
    def process_payment(self, amount):
        return f"Processed payment of {amount} using PayPal"

# Using polymorphism for different payment methods
def process_transaction(payment_method: PaymentMethod, amount: float):
    print(payment_method.process_payment(amount))

credit_card = CreditCard()
paypal = PayPal()

process_transaction(credit_card, 100.0)  # Outputs: Processed payment of 100.0 using Credit Card
process_transaction(paypal, 50.0)        # Outputs: Processed payment of 50.0 using PayPal
```

***

**7. Polymorphism with Method Overloading (using default arguments)**

```python
class Calculator:
    def add(self, a, b, c=0):
        return a + b + c

# Polymorphism with default arguments (method overloading)
calc = Calculator()
print(calc.add(1, 2))  # Outputs: 3
print(calc.add(1, 2, 3))  # Outputs: 6
```

***

**8. Polymorphism in a Drawing Application (Shape Classes)**

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

class Rectangle(Shape):
    def draw(self):
        return "Drawing Rectangle"

class Circle(Shape):
    def draw(self):
        return "Drawing Circle"

# Using polymorphism in the drawing application
def draw_shape(shape: Shape):
    print(shape.draw())

rectangle = Rectangle()
circle = Circle()

draw_shape(rectangle)  # Outputs: Drawing Rectangle
draw_shape(circle)  # Outputs: Drawing Circle
```

***

**9. Polymorphism with Duck Typing**

```python
class Dog:
    def speak(self):
        return "Bark"

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

# Duck typing - both classes have a speak method, but no common base class
def animal_speak(animal):
    print(animal.speak())

dog = Dog()
cat = Cat()

animal_speak(dog)  # Outputs: Bark
animal_speak(cat)  # Outputs: Meow
```

***

**10. Polymorphism in a Sorting Application (Sortable Classes)**

```python
class Sortable:
    def sort(self):
        pass

class Integer(Sortable):
    def __init__(self, value):
        self.value = value

    def sort(self):
        return self.value

class String(Sortable):
    def __init__(self, value):
        self.value = value

    def sort(self):
        return ''.join(sorted(self.value))

# Polymorphism in sorting different types
def sort_item(item: Sortable):
    print(item.sort())

integer = Integer(5)
string = String("hello")

sort_item(integer)  # Outputs: 5
sort_item(string)   # Outputs: ehllo
```

***

**Key Concepts:**

* **Method Overriding**: Subclasses provide their own implementation of methods defined in a parent class.
* **Polymorphism**: Objects of different classes can be treated uniformly when they implement the same method signature.
* **Duck Typing**: Python's dynamic nature allows polymorphism without requiring a strict class hierarchy.
* **Multiple Inheritance**: Polymorphism can extend to multiple parent classes, allowing more flexibility.
* **Abstract Base Classes (ABCs)**: Polymorphism can be enforced through abstract methods in base classes.

Polymorphism increases flexibility and reduces code complexity by allowing different classes to share a common interface and behave differently based on the actual class type at runtime.
