Python Polymorphism

Python polymorphism allows you to treat objects from different classes as if they belong to a common superclass. You’ll achieve this through method overriding, where you can customize methods inherited from a superclass in your subclasses, and method overloading, where you adapt a single method’s behavior based on its arguments.

This power simplifies your code and boosts reusability, especially when dealing with various object types that share a common interface. Polymorphism empowers you to handle diverse objects gracefully, without the need to know their specific types in advance.

Let’s imagine you’re developing software for a graphics application that deals with various shapes—circles, rectangles, and triangles. Each shape is represented as a class with a common method, calculate_area(), responsible for calculating its area. Python polymorphism comes into play here because, regardless of the specific shape’s class, you can call calculate_area() on any shape object seamlessly.

For example, you can calculate the area of a circle, a rectangle, or a triangle using the same method name. Behind the scenes, each shape class overrides calculate_area() with its unique formula. This approach makes it more intuitive, as you don’t need to write distinct code for each shape, yet you can work with them uniformly.

Now that you have a fundamental grasp of polymorphism in Python, let’s move forward and explore how this concept is put into practical use in programs, illustrated through syntax.

Python Polymorphism Syntax

The Python polymorphism syntax is clear and simple to comprehend. This is how it looks:

class ParentClass:
   def common_method(self):
      # Parent class method implementation

class ChildClass(ParentClass):
   def common_method(self):
      # Child class method implementation

In this syntax, ChildClass inherits from ParentClass and overrides the common_method defined in the parent class with its own implementation. This allows you to use polymorphism by calling common_method on objects of both ParentClass and ChildClass, with each class’s specific method being executed based on the object’s actual class.

Having gained a fundamental grasp of polymorphism and delved into its syntax, let’s now dive into practical examples to give you a clearer understanding of how this concept operates in real-world scenarios.

I. Python Inbuilt Polymorphic Function

In Python, you have access to a range of inbuilt polymorphic functions that can handle objects of different types, adjusting their actions according to the input they receive. These functions encompass familiar built-in functions like len() and str() along with operators like + and *.

This offered by inbuilt polymorphic functions empowers you to write code that’s adaptable, allowing you to work seamlessly with various data types using the same set of functions and operators. Let’s examine some of them below:

A. Python Polymorphic len() Function

The polymorphic len() function is used to evaluate the length or size of a sequence or collection, such as a string, list. It adapts its behavior based on the type of object passed to it, making it convenient way to find the number of elements or characters in various data structures.

This polymorphic nature of len() enables you to create code that functions consistently across various data structures, eliminating the need for explicit object type checks. For example:

Example Code
string_length = len("Hello, Python Helper!") print("Length of the string:", string_length) even_list = [0, 2, 4, 6, 8,10] list_length = len(even_list) print("Number of elements in the list:", list_length) my_tuple = (10, 20, 30, 40, 50) tuple_length = len(my_tuple) print("Number of elements in the tuple:", tuple_length)

For this example, we begin by initializing a variable string_length and using len() to evaluate length of  string Hello, Python Helper!. By simply passing the string as an argument to len(), we easily obtain the count of characters in the string, which is then printed out, giving us the string’s length.

Next, we transition to a list named even_list containing a sequence of even numbers. We apply  len() function to this list, which, once again, adapts itself to the data structure provided. Consequently, we receive the count of elements in the list, in this case, the number of even integers. The result is stored in list_length, and the outcome is displayed as Number of elements in the list: followed by the count.

Moving on, we employ the len() function with a tuple named my_tuple. Similar to the previous cases, the function adjusts its behavior to calculate the number of elements within the tuple. The result is stored in the tuple_length variable and presented to us in the form of Number of elements in the tuple:, followed by the count of elements in the tuple.

Output
Length of the string: 21
Number of elements in the list: 6
Number of elements in the tuple: 5

This example exemplifies how the polymorphic len() function makes it seamless to work with diverse data structures, enhancing code readability and reducing the need for explicit type checks.

B. Polymorphic str() Function

In Python, the polymorphic str() function serves to transform various object types into their respective string representations. It adapts its behavior based on the type of object passed to it, allowing you to obtain a string representation of integers, floats and custom objects.

The adaptability of str() function proves its worth when there’s a requirement to transform diverse data types into strings, whether it’s for presentation, logging, or any other use, all without the necessity for explicit type conversions. For instance:

Example Code
temperature_int = 25 temperature_str_int = str(temperature_int) print("Temperature as an integer:", temperature_str_int) temperature_float = 23.5 temperature_str_float = str(temperature_float) print("Temperature as a float:", temperature_str_float) temperature_list = [22, 24.5, 26, 21] temperature_str_list = str(temperature_list) print("List of temperatures:", temperature_str_list) temperature_dict = {"Monday": 24, "Tuesday": 23.5, "Wednesday": 25} temperature_str_dict = str(temperature_dict) print("Dictionary of temperatures:", temperature_str_dict)

Here, First, we initialize an integer variable temperature_int with the value 25, and then we use str() to convert it into a string representation, storing the result in temperature_str_int. By doing this, we transform the temperature from an integer to a string, making it suitable for various display or logging purposes. We print this converted temperature along with a descriptive label.

Next, we repeat a similar process with a float temperature. We set temperature_float to 23.5, use str() to convert it to a string (temperature_str_float), and print it with an appropriate label. This showcases how the str() function adapts to different data types, in this case, a floating-point temperature.

Moving on, we work with a list of temperatures stored in temperature_list. Using str(), we transform this list into its string representation, which is assigned to temperature_str_list. We print out this string representation along with a label. Lastly, we explore the use of str() with a dictionary of temperatures, temperature_dict. Again, the str() function is employed to convert the dictionary into its string representation, which is then stored in temperature_str_dict.

Output
Temperature as an integer: 25
Temperature as a float: 23.5
List of temperatures: [22, 24.5, 26, 21]
Dictionary of temperatures: {‘Monday’: 24, ‘Tuesday’: 23.5, ‘Wednesday’: 25}

As evident from this above example, it illustrates the smooth adaptability of Python’s str() function to easily transform different types of temperature data, encompassing integers, floats, lists, and dictionaries, into their corresponding string representations.

II. Python Polymorphism in Class Methods

Polymorphism in class methods enables you to define methods with the same name in different classes, each with its unique behavior. This allows you to use objects from these classes interchangeably when you call the shared method, even though the method’s implementation may differ between classes.

This concept is crucial in OOP, where base classes establish a common interface through method signatures, and derived classes provide their own implementations of these methods. Consider below illustration:

Example Code
class Animal: 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!" dog = Dog() cat = Cat() bird = Bird() print("Dog says:", dog.speak()) print("Cat says:", cat.speak()) print("Bird says:", bird.speak())

In this example, we have a set of Python classes that illustrate the concept of polymorphism in class methods. We start with a base class called Animal, which defines a method named speak() but doesn’t provide any specific implementation for it. This method acts as a placeholder, ensuring that all derived classes will have a speak() method.

Next, we create three derived classes: Dog, Cat, and Bird. Each of these classes inherits from the Animal class and provides its own unique implementation of the speak() method. This is where the power of polymorphism comes into play. Despite having the same method name, each subclass can have its distinct behavior. The Dog class makes the dog say Woof!, the Cat class makes the cat say Meow!, and the Bird class makes the bird say Chirp!.

We then proceed to create instances of these classes: dog, cat, and bird. When we call the speak() method on each instance and print the results, we observe that despite the method having the same name, it produces different outputs based on the class it belongs to.

Output
Dog says: Woof!
Cat says: Meow!
Bird says: Chirp!

This above example showcases how polymorphism allows objects of different classes to be treated as objects of a common base class while still having their own specialized behaviors.

Python Polymorphism Advanced Examples

Now that you’ve developed a solid grasp of Python polymorphism and have explored them in various scenarios, let’s delve into some advanced examples of this polymorphism. This exploration will provide you with a clearer picture of this concept, which holds significant value in OOP.

I. Polymorphism with Inheritance

You can also use polymorphism with inheritance, which is a fundamental concept in OOP, In this approach, you initiate a parent class that outlines a standardized interface or a group of methods. Then, you derive multiple subclasses from  superclass, each with its own specialized implementations of those methods.

Despite the differences in implementations, objects of these child classes can be treated as objects of the base class, allowing you to work with them in a uniform manner. This implies that you can write code that functions with the core class, and it will easily adjust to all subclasses, leveraging their unique behaviors when necessary. For example:

Example Code
class NumberGenerator: def __init__(self): self._numbers = [] def generate_numbers(self, limit): pass class PrimeNumberGenerator(NumberGenerator): def generate_numbers(self, limit): for number in range(2, limit + 1): if self.is_prime(number): self._numbers.append(number) def is_prime(self, num): if num < 2: return False for divisor in range(2, int(num**0.5) + 1): if num % divisor == 0: return False return True class FibonacciGenerator(NumberGenerator): def generate_numbers(self, limit): a, b = 0, 1 while a <= limit: self._numbers.append(a) a, b = b, a + b # Usage prime_generator = PrimeNumberGenerator() prime_generator.generate_numbers(30) print("Prime numbers up to 30:", prime_generator._numbers) fibonacci_generator = FibonacciGenerator() fibonacci_generator.generate_numbers(100) print("Fibonacci numbers up to 100:", fibonacci_generator._numbers)

For this example, we start with a base class called NumberGenerator, which has an empty list _numbers initialized in its constructor. The key feature of this base class is the generate_numbers(limit) method, which serves as a common interface for generating numbers up to a specified limit.

Next, we have two derived classes, PrimeNumberGenerator and FibonacciGenerator, both of which inherit from the base class NumberGenerator. The PrimeNumberGenerator class generates prime numbers up to the given limit using the Sieve of Eratosthenes algorithm, and it also has a helper method is_prime(num) to check if a number is prime. On the other hand, the FibonacciGenerator class generates Fibonacci numbers up to the specified limit.

We then proceed to use these classes. We create instances of PrimeNumberGenerator and FibonacciGenerator named prime_generator and fibonacci_generator, respectively. We invoke the generate_numbers(limit) method on each of them, specifying the limit. Finally, we print out the generated numbers for prime and Fibonacci sequences.

Output
Prime numbers up to 30: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Fibonacci numbers up to 100: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

As you can see, you can readily apply polymorphism with inheritance in your code, enabling you to enhance its sophistication and execute intricate computations within it.

II. Polymorphism using Method Overriding

In polymorphism, when you use method overriding, you’re essentially letting a subclass create its own version of a method inherited from its superclass. This way, you can customize how a method works for specific subclasses while keeping a uniform method interface for related classes. For instance:

Example Code
class Book: def __init__(self, title, author, color, year): self.title = title self.author = author self.color = color self.year = year def display_info(self): return f"'{self.title}' by {self.author}, {self.color} cover, published in {self.year}" class HardcoverBook(Book): def display_info(self): return f"Hardcover: {super().display_info()}" class PaperbackBook(Book): def display_info(self): return f"Paperback: {super().display_info()}" hardcover = HardcoverBook("The Great Gatsby", "F. Scott Fitzgerald", "Green", 1925) paperback = PaperbackBook("To Kill a Mockingbird", "Harper Lee", "Blue", 1960) print(hardcover.display_info()) print(paperback.display_info())

Here, we define a Book class, which has a constructor method __init__ that initializes attributes such as title, author, color, and year for a generic book object. Additionally, the Book class includes a method named display_info, which returns a formatted string representing information about the book, including its title, author, cover, color, and publication year.

Next, we create two subclasses, HardcoverBook and PaperbackBook, both of which inherit from the base class Book. These subclasses override the display_info method to add specific details about the type of book they represent. For example, the HardcoverBook subclass prefixes the information with Hardcover, and the PaperbackBook subclass prefixes it with Paperback. To achieve this, they call the super().display_info() method from the base class and append their respective prefixes.

We then create instances of both HardcoverBook and PaperbackBook, each representing a different book with unique attributes. Finally, we call the display_info method on these instances and print the formatted book information.

Output
Hardcover: ‘The Great Gatsby’ by F. Scott Fitzgerald, Green cover, published in 1925
Paperback: ‘To Kill a Mockingbird’ by Harper Lee, Blue cover, published in 1960

This approach illustrates that the same method display_info behaves differently based on the specific subclass of Book used, providing tailored book descriptions for each type of book while utilizing the principle of method overriding.

III. Exception Handling with Polymorphism

It allows you to handle errors and exceptional situations gracefully by using polymorphic methods to catch and manage exceptions in a unified manner. When you have a hierarchy of classes with overridden methods, you can use polymorphism to create a consistent approach to handling exceptions across different subclasses.

Instead of writing separate exception handling code for each subclass, you can define a common exception handling method in the base class, and each subclass can override this method to provide its specific error-handling behavior. Consider an illustration below:

Example Code
class BaseExceptionHandling: def handle_exception(self, exception): return f"Base Exception Handling: {str(exception)}" class CustomExceptionHandling1(BaseExceptionHandling): def handle_exception(self, exception): return f"Custom Exception Handling 1: {str(exception)}" class CustomExceptionHandling2(BaseExceptionHandling): def handle_exception(self, exception): return f"Custom Exception Handling 2: {str(exception)}" def process_data(data, handler): try: result = 10 / data # Simulating a division by zero exception return f"Result: {result}" except Exception as e: return handler.handle_exception(e) # Usage handler1 = CustomExceptionHandling1() handler2 = CustomExceptionHandling2() data = 0 # Causes a division by zero exception print(process_data(data, handler1)) print(process_data(data, handler2))

In this example, we have a scenario where we want to handle exceptions differently based on specific exception handling strategies. To achieve this, we utilize polymorphism in Python. We begin by defining a BaseExceptionHandling, which includes a method handle_exception responsible for handling exceptions. This base class establishes a common interface for exception handling.

Next, we create two custom exception handling classes, CustomExceptionHandling1 and CustomExceptionHandling2, both of which inherit from the base class BaseExceptionHandling. These custom classes override the handle_exception method, providing their own unique exception handling implementations.

Now, we have the process_data function, which takes two arguments: data and handler. Inside this function, we attempt a division operation by dividing 10 by data. However, we simulate a division by zero exception, which could occur if data is set to 0. In case of an exception, we catch it using a try-except block, and instead of handling the exception directly, we delegate the handling to the handler object provided as an argument. The handler object is expected to be an instance of one of the custom exception handling classes, which showcase polymorphism.

In the usage section, we create two different handlers, handler1 and handler2, each representing a unique exception handling strategy. We then set data to 0, deliberately causing a division by zero exception. We call the process_data function twice, passing the same data value and different handlers (handler1 and handler2) as arguments.  When we run the code, it prints out the results of handling the exception with both handler1 and handler2, showcasing the flexibility of polymorphism in exception handling.

Output
Custom Exception Handling 1: division by zero
Custom Exception Handling 2: division by zero

In conclusion, this example illustrates how polymorphism can be employed to implement diverse exception handling strategies based on the specific needs of different situations, enhancing code modularity and maintainability.

Now that you have gained a firm grasp of Python polymorphism and have explored them in various scenarios, let’s delve into the theoretical aspects of polymorphism. Understanding these theoretical concepts is crucial in programming as they play a significant role in shaping your coding practices and overall programming knowledge.

Advantages of Polymorphism

Certainly, here are the advantages of polymorphism:

I. Code Reusability

With polymorphism, you can reuse code that’s written for a base class with its derived classes, reducing redundancy and making your code more efficient.

II. Flexibility

Python Polymorphism allows you to work with objects of different classes through a common interface, making your code more adaptable to changes and additions of new classes.

III. Enhanced Readability

It improves code readability as you can write generic code that can work with multiple types of objects, making the code easier to understand.

Congratulations on exploring Python polymorphism! You’ve uncovered a remarkable capability that streamlines your code and boosts its flexibility and convenience. And there’s an added bonus – you can tap into built-in polymorphic functions such as len() and str() that adapt their behavior according to the data type they encounter.

Furthermore, you’ve delved into advanced scenarios like polymorphism with inheritance, making intricate coding a breeze. You’ve also dived into method overriding, a nifty polymorphic technique that empowers subclasses to craft their own versions of inherited methods, lending uniqueness while preserving a consistent interface.

Lastly, you’ve tackled exception handling with polymorphism, streamlining the process. Don’t forget to explore the theoretical aspects too. Python Polymorphism brings a multitude of benefits, from code reuse and adaptability to improved code readability and simplified maintenance. Keep on this path of exploration and leverage polymorphism to craft efficient and adaptable Python code. Your programming journey just took a thrilling turn!

 
Scroll to Top