Polymorphism

Python Mastery: From Beginner to Expert - Sykalo Eugene 2023

Polymorphism
Object-oriented programming

Introduction to Polymorphism

Polymorphism is a key concept in object-oriented programming that allows objects to take on multiple forms. This means that a single object can be treated as if it were multiple different objects, depending on the context in which it is used.

In Python, polymorphism is achieved through the use of functions, methods, and classes. For example, we can define a function that can take arguments of different types and perform different actions depending on the type of the argument. We can also define a method in a class that behaves differently depending on the specific subclass that is calling it.

There are several types of polymorphism, including:

  • Ad-hoc Polymorphism: This is also known as method overloading or operator overloading, and it allows multiple methods or operators to have the same name, but different parameters or behaviors.
  • Parametric Polymorphism: This allows a function or method to accept arguments of any type.
  • Subtype Polymorphism: This allows a subclass to be treated as if it were its parent class, allowing for greater flexibility and modularity in code.

Polymorphism has several advantages, including increased flexibility and reusability of code. By designing code to be polymorphic, we can create more modular and extensible systems that are easier to maintain and modify over time.

Polymorphism with Functions

Function Overloading

Function overloading is a type of ad-hoc polymorphism that allows multiple functions to have the same name, but different parameters or behaviors. This means that we can define two or more functions with the same name, but different numbers or types of arguments, and Python will determine which function to call based on the arguments that are passed in.

For example, we can define two functions with the same name, add_numbers, but with different numbers of arguments:

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

def add_numbers(x, y, z):
 return x + y + z

print(add_numbers(1, 2)) # Output: 3
print(add_numbers(1, 2, 3)) # Output: 6

In this example, the first add_numbers function takes two arguments, while the second takes three. When we call add_numbers with two arguments, Python will call the first function, and when we call it with three arguments, Python will call the second function.

Function Overriding

Function overriding is a type of polymorphism that occurs when a subclass defines a method with the same name as a method in its parent class. When the method is called on an object of the subclass, the subclass's method is called instead of the parent class's method.

For example, we can define a class called Animal with a method called make_sound, and then define a subclass called Dog that overrides the make_sound method:

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

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

animal = Animal()
dog = Dog()

animal.make_sound() # Output: "The animal makes a sound"
dog.make_sound() # Output: "The dog barks"

In this example, when we call the make_sound method on the animal object, the Animal class's make_sound method is called. But when we call the make_sound method on the dog object, the Dog class's make_sound method is called instead.

Function overriding allows us to create more specialized behavior for subclasses, while still maintaining the same interface as the parent class. This makes our code more modular and easier to maintain over time.

Polymorphism with Methods

In addition to using polymorphism with functions, we can also use it with methods in classes. There are two main types of polymorphism with methods: method overloading and method overriding.

Method Overloading

Method overloading is similar to function overloading, but it occurs within a class. It allows multiple methods in a class to have the same name, but different parameters or behaviors. When a method is called with a certain set of parameters, Python will determine which method to call based on the parameters that are passed in.

For example, we can define a class called Calculator with two add methods that take different numbers of arguments:

class Calculator:
 def add(self, x, y):
 return x + y

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

calculator = Calculator()

print(calculator.add(1, 2)) # Output: 3
print(calculator.add(1, 2, 3)) # Output: 6

In this example, the Calculator class has two add methods, one that takes two arguments and one that takes three. When we call the add method with two arguments, Python will call the first method, and when we call it with three arguments, Python will call the second method.

Method Overriding

Method overriding is similar to function overriding, but it occurs within a class hierarchy. It allows a subclass to provide a specific implementation of a method that is already provided by its parent class. When the method is called on an object of the subclass, the subclass's method is called instead of the parent class's method.

For example, we can define a class called Vehicle with a method called start, and then define a subclass called Car that overrides the start method:

class Vehicle:
 def start(self):
 print("The vehicle starts")

class Car(Vehicle):
 def start(self):
 print("The car starts")

vehicle = Vehicle()
car = Car()

vehicle.start() # Output: "The vehicle starts"
car.start() # Output: "The car starts"

In this example, when we call the start method on the vehicle object, the Vehicle class's start method is called. But when we call the start method on the car object, the Car class's start method is called instead.

Method overriding allows us to create more specialized behavior for subclasses, while still maintaining the same interface as the parent class. This makes our code more modular and easier to maintain over time.

Polymorphism with Classes

In addition to using polymorphism with functions and methods, we can also use it with classes. There are several ways to achieve polymorphism with classes in Python, including:

Duck Typing

Duck typing is a concept in Python that allows us to treat any object as if it were of a certain type, as long as it has the attributes and methods that are expected of that type. This means that we can write code that works with multiple different types of objects, as long as they have the same interface.

For example, we can define a function that takes an object as an argument and calls a method on it:

def make_sound(animal):
 animal.make_sound()

This function doesn't care what type of object is passed in, as long as it has a make_sound method. We can pass in an instance of the Animal class or the Dog class from the previous examples, and both will work:

animal = Animal()
dog = Dog()

make_sound(animal) # Output: "The animal makes a sound"
make_sound(dog) # Output: "The dog barks"

In this example, we're using duck typing to allow the make_sound function to work with multiple different types of objects, as long as they have a make_sound method.

Operator Overloading

Operator overloading is a type of ad-hoc polymorphism that allows us to define how operators like +, -, *, /, and % behave for objects of a certain class. We do this by defining special methods in the class that correspond to each operator.

For example, we can define a class called Vector that represents a mathematical vector with x and y components. We can then define the add method to allow us to add two vectors together using the + operator:

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)

In this example, we're defining the add method to add two vectors together by adding their x and y components. We can then create two Vector objects and add them together using the + operator:

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

v3 = v1 + v2

print(v3.x) # Output: 4
print(v3.y) # Output: 6

In this example, we're using operator overloading to allow us to add two Vector objects together using the + operator.

Polymorphism with classes allows us to create more modular and extensible code by designing code that can work with multiple different types of objects. By using polymorphism, we can create code that is more flexible, reusable, and easier to maintain over time.

Polymorphism in Built-In Functions

Python provides several built-in functions that can take arguments of different types and behave differently depending on the type of the argument. This is an example of parametric polymorphism, which allows a function or method to accept arguments of any type.

len()

The len() function is used to get the length of an object. It can be used with many different types of objects, including strings, lists, tuples, dictionaries, and sets. When called with an object, len() returns the number of items in the object.

string = "Hello, world!"
list = [1, 2, 3, 4, 5]
dictionary = {"one": 1, "two": 2, "three": 3}

print(len(string)) # Output: 13
print(len(list)) # Output: 5
print(len(dictionary)) # Output: 3

In this example, we're using the len() function with a string, a list, and a dictionary to get the number of items in each object.

str()

The str() function is used to convert an object to a string. It can be used with many different types of objects, including numbers, lists, tuples, dictionaries, and sets. When called with an object, str() returns a string representation of the object.

number = 42
list = [1, 2, 3, 4, 5]
dictionary = {"one": 1, "two": 2, "three": 3}

print(str(number)) # Output: "42"
print(str(list)) # Output: "[1, 2, 3, 4, 5]"
print(str(dictionary)) # Output: "{'one': 1, 'two': 2, 'three': 3}"

In this example, we're using the str() function with a number, a list, and a dictionary to get string representations of each object.

type()

The type() function is used to get the type of an object. It can be used with many different types of objects, including numbers, strings, lists, tuples, dictionaries, and sets. When called with an object, type() returns the type of the object.

number = 42
string = "Hello, world!"
list = [1, 2, 3, 4, 5]
dictionary = {"one": 1, "two": 2, "three": 3}

print(type(number)) # Output: <class 'int'>
print(type(string)) # Output: <class 'str'>
print(type(list)) # Output: <class 'list'>
print(type(dictionary)) # Output: <class 'dict'>

In this example, we're using the type() function with a number, a string, a list, and a dictionary to get the type of each object.

Polymorphism in built-in functions allows us to write code that can work with many different types of objects, making our code more modular and extensible. By designing code that is parametrically polymorphic, we can write code that is more flexible, reusable, and easier to maintain over time.

Real-World Examples of Polymorphism

Polymorphism in GUI Applications

Graphical user interface (GUI) applications often make use of polymorphism to create reusable and extensible code. For example, a GUI application might have a base class called Widget that defines common properties and methods for all widgets, such as size, position, and visibility. Then, specific types of widgets, such as buttons, labels, and input fields, can be defined as subclasses of the Widget class, inheriting its properties and methods.

By using polymorphism, GUI applications can create a modular and flexible architecture that allows for easy customization and extension. For example, if a new type of widget needs to be added to the application, it can be easily defined as a subclass of the Widget class, without the need to modify existing code.

Polymorphism in Web Development

Web development frameworks, such as Django and Flask, make use of polymorphism to create reusable and extensible code. For example, a web application might have a base class called View that defines common properties and methods for all views, such as handling HTTP requests and rendering templates. Then, specific types of views, such as home pages, login pages, and profile pages, can be defined as subclasses of the View class, inheriting its properties and methods.

By using polymorphism, web development frameworks can create a modular and flexible architecture that allows for easy customization and extension. For example, if a new type of view needs to be added to the application, it can be easily defined as a subclass of the View class, without the need to modify existing code.

In both GUI applications and web development, polymorphism allows for greater flexibility and modularity in code, making it easier to maintain and modify over time. By designing code to be polymorphic, developers can create more extensible and reusable systems that are better suited to changing requirements and evolving user needs.