Python Polymorphism

Python polymorphism means "many forms". In simple terms, it's when you can use the same function name or operator to do different things depending on the type of data you're using. It's like having a universal remote that works for your TV, stereo, and other devices. This makes your code more flexible and easier to understand.
Table of Contents

What is Polymorphism in Python?

Python polymorphism means “many forms”. In simple terms, it lets you use the same function or operator on different types of objects. For example, the len() function can find the length of a string like “hello” (which is 5) or the number of items in a list like [1, 2, 3] (which is 3). It performs differently depending on whether it receives a string or a list, but you use the same function name, len(). This makes your code more flexible and reusable.

Example of Python Polymorphism

def add(x, y):
  return x + y

print(add(5, 3))     # Output: 8
print(add(5.5, 2.5)) # Output: 8.0
print(add("Hello", " World")) # Output: Hello World

Explanation

  1. The add Function:
    • We define a function called def add(x, y):. This function takes two inputs, x and y.
    • Inside the function, we have return x + y. This means the function will return the result of using the + operator on x and y.
  2. Polymorphism:
    • Adding Numbers:
      • In the line print(add(5, 3)), we call the add function with two integers, 5 and 3.
      • The + operator, in this case, performs addition. So, 5 + 3 equals 8, and that’s what gets printed.
      • Similarly, in print(add(5.5, 2.5)) we are adding two floating point numbers resulting in 8.0.
    • Concatenating Strings:
      • In the line print(add("Hello", " World")), we call the add function with two strings, "Hello" and " World".
      • Here, the same + operator now performs string concatenation. It joins the two strings together.
      • So, "Hello" + " World" becomes "Hello World", and that’s what gets printed.

The add function and the + operator are showing polymorphism. They behave differently depending on the type of data you give them. With numbers, + adds. With strings, + concatenates.


Function Polymorphism in Python

Function polymorphism refers to functions that can operate on different types of data. Built-in functions like len() are great examples of this because they can get the length of different types of sequences, such as lists, tuples, strings, or dictionaries. The function works differently depending on the object type passed.

Syntax

length = len(object)

Explanation

  • length = ...: The result of the len() function will be stored in a variable called length.
  • len(object): The built-in len() function is called with object as its argument. The behavior of len() varies depending on the type of object.

Example

my_list = [1, 2, 3, 4]
my_string = "Hello"
my_tuple = (5, 6, 7)

print(len(my_list))    # Output: 4
print(len(my_string))  # Output: 5
print(len(my_tuple))   # Output: 3

Explanation

  • my_list = [1, 2, 3, 4]: A list named my_list is created.
  • my_string = "Hello": A string named my_string is created.
  • my_tuple = (5, 6, 7): A tuple named my_tuple is created.
  • print(len(my_list)): The len() function calculates the length of my_list (which is 4).
  • print(len(my_string)): The len() function calculates the length of my_string (which is 5).
  • print(len(my_tuple)): The len() function calculates the length of my_tuple (which is 3).

Class Polymorphism in Python

Class polymorphism is when different classes have methods with the same name. This allows you to call the same method on objects of different classes, and each object will respond appropriately based on its class definition. Python polymorphism with class methods enables you to write more generic code that can handle objects of various classes without needing separate logic for each class type.

Example

class Animal:
    def speak(self):
        print("Generic animal sound")

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

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

animals = [Animal(), Dog(), Cat()]
for animal in animals:
    animal.speak()

Explanation

  • class Animal:: Defines a base class Animal.
  • def speak(self):: Defines a method speak in the Animal class.
  • print("Generic animal sound"): The base class’s speak method prints a generic sound.
  • class Dog(Animal): and class Cat(Animal):: Define two classes, Dog and Cat, that inherit from Animal.
  • def speak(self):: Each derived class overrides the speak method.
  • print("Woof!") and print("Meow!"): The Dog and Cat classes print their specific sounds.
  • animals = [Animal(), Dog(), Cat()]: Creates a list containing instances of all three classes.
  • for animal in animals:: Loops through the list of animals.
  • animal.speak(): Calls the speak method on each animal. Because of polymorphism, the correct version of speak is called for each object.

Polymorphism and Inheritance (Method Overriding)

Method overriding is a key feature in object-oriented programming, and it’s a crucial aspect of polymorphism in Python. It allows a subclass to provide a specific implementation of a method already defined in its superclass. When a method in a subclass has the same name, same parameters, and same return type as a method in its superclass, then the subclass’s method overrides the superclass’s method. Python polymorphism, through method overriding, enables you to customize the behavior of inherited methods to fit the specific needs of the subclass.

Syntax

class ParentClass:
  method my_method():
    # Parent class implementation

class ChildClass(ParentClass):
  method my_method():
    # Child class implementation (overrides the parent's method)

Explanation

  • class ParentClass:: This defines the parent class.
  • method my_method():: This is a method defined in the ParentClass.
  • class ChildClass(ParentClass):: This defines the child class, which inherits from ParentClass.
  • method my_method():: This is a method within ChildClass that has the same name as the method in ParentClass. The child’s method will override the parent’s method when called on a ChildClass object.

Example

class Animal:
    def make_sound(self):
        print("Generic animal sound")

class Dog(Animal):
    def make_sound(self):
        print("Woof!")

animal = Animal()
dog = Dog()

animal.make_sound()  # Output: Generic animal sound
dog.make_sound()     # Output: Woof!

Explanation

  • class Animal:: Defines a base class called Animal.
  • def make_sound(self):: Defines a method make_sound in the Animal class.
  • print("Generic animal sound"): This is the default behavior of make_sound in the base class.
  • class Dog(Animal):: Defines a subclass Dog that inherits from Animal.
  • def make_sound(self):: This method in Dog overrides the make_sound method of Animal.
  • print("Woof!"): The Dog class provides its own implementation of make_sound.
  • animal = Animal() and dog = Dog(): Creates instances of Animal and Dog.
  • animal.make_sound(): Calls make_sound on the Animal object, printing the generic sound.
  • dog.make_sound(): Calls make_sound on the Dog object. Due to method overriding, the Dog‘s specific sound (“Woof!”) is printed.

Polymorphism in Python through Operator Overloading

Operator overloading is a technique that exemplifies polymorphism in Python. It allows you to define how operators behave with objects of your classes. For example, you can specify what happens when you use the + operator to add two custom class objects. This customization of operator behavior for different classes is an important aspect of Python polymorphism.

Polymorphism in + operator

The + operator provides a great example of polymorphism through operator overloading. When used with numbers, + performs addition. With strings, it performs concatenation. You can also define how the + operator works with objects of your classes by implementing the __add__ method. This ability of the + operator to adapt its behavior based on the operands is a clear demonstration of Python polymorphism.

Syntax

class MyClass:
  method __add__(self, other):
    # Define how '+' works for objects of MyClass

Explanation

  • class MyClass:: Defines a class where you want to customize the behavior of the + operator.
  • method __add__(self, other):: This is a special method that gets called when the + operator is used with an object of MyClass on the left-hand side. It takes the object on the right-hand side of the + operator as the other argument.

Example

class ComplexNumber:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

    def __add__(self, other):
        return ComplexNumber(self.real + other.real, self.imag + other.imag)

    def __str__(self):
        return f"{self.real} + {self.imag}i"

c1 = ComplexNumber(2, 3)
c2 = ComplexNumber(1, 4)
c3 = c1 + c2  # Uses the overloaded '+' operator
print(c3)  # Output: 3 + 7i

Explanation

  • class ComplexNumber:: Defines a class to represent complex numbers.
  • def __init__(self, real, imag):: The constructor initializes the real and imaginary parts.
  • self.real = real and self.imag = imag: Assigns the real and imaginary parts to the object.
  • def __add__(self, other):: This method overloads the + operator for ComplexNumber objects.
  • return ComplexNumber(self.real + other.real, self.imag + other.imag): Defines that adding two ComplexNumber objects returns a new ComplexNumber whose real and imaginary parts are the sums of the corresponding parts of the operands.
  • def __str__(self):: Defines how a ComplexNumber should be represented as a string.
  • return f"{self.real} + {self.imag}i": Returns a string in the format “real + imaginaryi”.
  • c1 = ComplexNumber(2, 3) and c2 = ComplexNumber(1, 4): Creates two ComplexNumber objects.
  • c3 = c1 + c2: Adds c1 and c2 using the overloaded + operator.
  • print(c3): Prints the resulting ComplexNumber using the overloaded __str__ method.

Polymorphism in * operator

The * operator also shows polymorphism. With numbers, it performs multiplication. With a string and an integer, it performs string repetition. You can even define how * works with your classes by implementing the __mul__ method. This adaptability of the * operator based on the context of its use is a core aspect of Python polymorphism.

Syntax

class MyClass:
  method __mul__(self, other):
    # Define how '*' works for objects of MyClass

Explanation

  • class MyClass:: Defines a class where you intend to customize the behavior of the * operator.
  • method __mul__(self, other):: This special method is invoked when the * operator is used with an object of MyClass on the left-hand side. The object on the right-hand side of * is passed as the other argument.

Example

class StringMultiplier:
    def __init__(self, string):
        self.string = string

    def __mul__(self, times):
        return self.string * times

    def __str__(self):
        return self.string

s = StringMultiplier("Hello")
result = s * 3  # Uses the overloaded '*' operator
print(result)  # Output: HelloHelloHello

Explanation

  • class StringMultiplier:: Defines a class that will handle string multiplication.
  • def __init__(self, string):: The constructor initializes the string.
  • self.string = string: Assigns the given string to the object.
  • def __mul__(self, times):: This method overloads the * operator.
  • return self.string * times: Defines that multiplying a StringMultiplier object by an integer will repeat the string that many times.
  • def __str__(self):: Defines how a StringMultiplier object should be represented as a string.
  • return self.string: Returns the stored string.
  • s = StringMultiplier("Hello"): Creates a StringMultiplier object with the string “Hello”.
  • result = s * 3: Multiplies the StringMultiplier object by 3, using the overloaded * operator.
  • print(result): Prints the result, which is the string repeated three times.

Disadvantages of Inheritance and Polymorphism

  • Tight Coupling: Inheritance can create strong dependencies between parent and child classes.
  • Base Class Problem: Changes in a base class can unintentionally break derived classes.
  • Complex Hierarchies: Overuse of inheritance can lead to complicated and hard-to-manage class structures.
  • Code Predictability: Polymorphism can make it harder to predict the exact behavior of code during runtime.
  • Increased Debugging Effort: Debugging can be more challenging due to the dynamic nature of polymorphism.
  • Runtime Errors: Polymorphism’s type checking at runtime can lead to errors not caught during development.

Conclusion

Python polymorphism helps write better code. It lets you use the same function or operator differently, depending on the object you’re working with. This makes code more flexible and easier to reuse. Polymorphism in Python, achieved through method overriding or operator overloading, helps you write cleaner and more organized programs.


Python Reference

Python Polymorphism

Table of Contents