😘Polymorphism

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.

Types of Polymorphism

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.

Inheritance-based Polymorphism

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:

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.

a. Method Overriding

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:

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.

b. Polymorphic Methods

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:

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

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:

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

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:

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

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:

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

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:

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.

Function Polymorphism in Python

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:

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:

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.

Class Polymorphism in Python

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:

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:

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:

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:

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

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:

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.

Last updated