Polymorphism
Polymorphism is one of the fundamental concepts of object-oriented programming (OOP), which allows objects of different classes to be used interchangeably in the same context. In Python, polymorphism can be achieved through two different mechanisms: function overloading and method overriding.
- Function Overloading: Function overloading refers to the ability to define multiple functions with the same name but different parameters. In Python, function overloading is not supported directly because Python functions can have any number of parameters with default values, and the interpreter determines which function to call based on the parameters passed at runtime. However, you can mimic function overloading in Python by using variable-length arguments or default parameters.
For example:
def add(a, b):
return a + b
def add(a, b, c):
return a + b + c
# This will result in a TypeError because the function signature does not match any of the defined functions
add(1, 2)
- Method Overriding: Method overriding refers to the ability of a subclass to provide its own implementation of a method that is already defined in its superclass. This allows the subclass to inherit the behavior of the superclass and customize it as needed. In Python, method overriding is achieved by defining a method with the same name in the subclass as in the superclass.
For example:
class Animal:
def make_sound(self):
print("Generic animal sound")
class Dog(Animal):
def make_sound(self):
print("Woof!")
class Cat(Animal):
def make_sound(self):
print("Meow")
# Create instances of each class and call the make_sound() method
animal = Animal()
dog = Dog()
cat = Cat()
animal.make_sound() # "Generic animal sound"
dog.make_sound() # "Woof!"
cat.make_sound() # "Meow"
In this example, the Animal class defines a make_sound() method that prints a generic animal sound. The Dog and Cat classes override this method to provide their own implementation of the sound. When the make_sound() method is called on an instance of each class, the appropriate sound is printed.
Duck typing
Duck typing is a programming concept used in dynamic programming languages such as Python. It is based on the idea that the type of an object is determined by its behavior or the methods it supports, rather than its actual type. This concept is often summarized as “If it walks like a duck and quacks like a duck, then it must be a duck.”
In other words, duck typing allows a method or function to accept any object that supports the required methods, regardless of its actual class or type. This makes it easier to write reusable and flexible code that can work with different types of objects.
For example, consider the following code:
class Car:
def drive(self):
print("Driving a car")
class Bike:
def ride(self):
print("Riding a bike")
def commute(vehicle):
vehicle.drive()
vehicle.ride()
car = Car()
bike = Bike()
commute(car) # This will work because the car object has a drive() method
commute(bike) # This will raise an AttributeError because the bike object does not have a drive() method
In this example, we have defined two classes: Car and Bike, each with its own unique methods. We have also defined a function commute() that takes a vehicle object as its parameter and calls the drive() and ride() methods on it.
When we pass the car object to the commute() function, it works as expected because the car object has a drive() method. However, when we pass the bike object to the commute() function, it raises an AttributeError because the bike object does not have a drive() method.
This is an example of duck typing in action. The commute() function does not care about the actual type or class of the vehicle object. It only cares about whether the object has the necessary methods to perform the required actions.
Duck typing can be a powerful tool for writing flexible and reusable code, but it can also be prone to errors if not used carefully. It is important to ensure that the objects passed to a function or method have the necessary methods, or to handle any errors that may arise if they do not.
Operator overloading and magic methods
In Python, operator overloading allows you to define the behavior of operators such as +, -, *, /, ==, and others for custom classes. It is a way to extend the functionality of the built-in operators to work with objects of custom classes.
Operator overloading is achieved using special methods called “magic methods” or “dunder methods” (short for “double underscore” methods). These methods are used to define the behavior of the operators when applied to objects of the class. For example, the __add__() method is used to define the behavior of the + operator.
Here are some commonly used magic methods for operator overloading in Python:
__add__(self, other): Defines the behavior of the + operator.__sub__(self, other): Defines the behavior of the – operator.__mul__(self, other): Defines the behavior of the * operator.__truediv__(self, other): Defines the behavior of the / operator.__floordiv__(self, other): Defines the behavior of the // operator.__mod__(self, other): Defines the behavior of the % operator.__lt__(self, other): Defines the behavior of the < operator.__le__(self, other): Defines the behavior of the <= operator.__eq__(self, other): Defines the behavior of the == operator.__ne__(self, other): Defines the behavior of the != operator.__gt__(self, other): Defines the behavior of the > operator.__ge__(self, other): Defines the behavior of the >= operator.
Here’s an example of operator overloading in action:
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, other):
return Vector(self.x * other, self.y * other)
def __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # prints (4, 6)
print(v1 - v2) # prints (-2, -2)
print(v1 * 2) # prints (2, 4)
In this example, we have defined a Vector class with x and y attributes, and implemented the __add__(), __sub__(), and __mul__() magic methods to define the behavior of the +, -, and * operators. We have also defined a __str__() method to print the vector as a string.
When we add, subtract, or multiply vectors using the +, -, and * operators, the behavior is defined by the __add__(), __sub__(), and __mul__() methods that we have defined. This allows us to perform these operations on objects of the Vector class just like we would on built-in types like integers or floats.
Operator overloading can be a powerful tool for creating more expressive and intuitive code, but it should be used sparingly and with caution. Overloading operators in unexpected ways can make code harder to read and maintain, and can lead to subtle bugs and errors. It is important to follow best practices and guidelines when using
Exercise
Implement an operator overloaded class to handle matrix multiplication