OOP part2#
3. Polymorphism:#
Polymorphism refers to the ability of different objects to respond to the same message or method call in different ways. In other words, polymorphism allows objects of different classes to be treated as objects of a common superclass, enabling code to be written in a generic way that can operate on objects of different types. There are two main types of polymorphism:
Compile-time Polymorphism (Static Binding): Also known as method overloading and operator overloading, compile-time polymorphism occurs when different implementations of a method or operator are provided for different parameter types or numbers.
Example of method overloading:
class MathOperations:
def add(self, a, b):
return a + b
def add(self, a, b, c):
return a + b + c
math = MathOperations()
# print(math.add(2, 3)) # Output: Error - Second add method is overwritten
print(math.add(2, 3, 4)) # Output: 9
9
Run-time Polymorphism (Dynamic Binding): Also known as method overriding, run-time polymorphism occurs when a subclass provides a specific implementation of a method that is already defined in its superclass.
Example of method overriding:
# Method Overriding
class Phone:
def __init__(self, price, brand, camera):
print ("Inside phone constructor")
self.__price = price
self.brand = brand
self.camera = camera
def buy(self):
print ("Buying a phone")
class SmartPhone(Phone):
def buy(self):
print ("Buying a smartphone")
s=SmartPhone(20000, "Apple", 13)
s.buy()
Inside phone constructor
Buying a smartphone
Operator Overloading:#
Operator overloading refers to the ability to redefine the behavior of operators such as +
, -
, *
, /
, etc., for user-defined classes. By overloading operators, objects of user-defined classes can support arithmetic, comparison, and other operations just like built-in types.
Example of operator overloading:
'hello' + 'world'
'helloworld'
4 + 5
9
[1,2,3] + [4,5]
[1, 2, 3, 4, 5]
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 __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2) # Output: (6, 8)
(6, 8)
In this example, the __add__()
method is overloaded to define the addition operation for objects of the Vector
class. When v1 + v2
is executed, Python calls the __add__()
method of the v1
object and passes v2
as the other
parameter.
Polymorphism and operator overloading enhance code readability, maintainability, and expressiveness by allowing for the creation of more intuitive and concise code. They are powerful features of object-oriented programming that enable developers to write more flexible and reusable code.
Magic Functions/Dunder Functions#
Magic functions, also known as dunder (double underscore) functions or special methods, are predefined methods in Python classes that have double underscores (__
) at the beginning and end of their names. These methods allow classes to define behavior that will be invoked in response to certain language-specific operations, such as arithmetic operations, comparison operations, object creation, and more.
Magic functions are often used to customize the behavior of objects and enable them to interact with operators, functions, and built-in language constructs in a way thatâs intuitive and consistent with Pythonâs syntax and semantics.
Dynamic Polymorphism#
Dynamic polymorphism, also known as runtime polymorphism or late binding, is a feature in object-oriented programming that allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This allows for the same method name to behave differently based on the type of object calling it.
Dynamic polymorphism enables a more flexible and extensible design, as it allows code to be written in a way that can operate on objects of different types without the need for explicit type checking.
Hereâs an example to illustrate dynamic polymorphism in Python:
class Animal:
def sound(self):
print("Animal makes a sound")
class Dog(Animal):
def sound(self):
print("Dog barks")
class Cat(Animal):
def sound(self):
print("Cat meows")
# Example of dynamic polymorphism
def make_sound(animal):
animal.sound()
# Create instances of different classes
animal1 = Animal()
animal2 = Dog()
animal3 = Cat()
# Call make_sound function with objects of different classes
make_sound(animal1) # Output: Animal makes a sound
make_sound(animal2) # Output: Dog barks
make_sound(animal3) # Output: Cat meows
Animal makes a sound
Dog barks
Cat meows
In this example:
We have a base class
Animal
with a methodsound()
.We have two subclasses
Dog
andCat
that override thesound()
method with their own implementations.The
make_sound()
function takes anAnimal
object as a parameter and calls itssound()
method.When
make_sound()
is called with objects of different subclasses, the appropriate implementation of thesound()
method is invoked based on the actual type of the object at runtime. This is dynamic polymorphism in action.
Dynamic polymorphism allows for more flexible and modular code, as it allows different objects to respond to the same method call in different ways based on their specific implementations. This promotes code reuse, simplifies design, and enhances maintainability.
4. Abstract Method and Class:#
In object-oriented programming, an abstract method is a method declared in a class definition, but it doesnât provide an implementation in the class itself. Instead, the implementation is expected to be provided by subclasses. Similarly, an abstract class is a class that cannot be instantiated directly and may contain one or more abstract methods. Abstract classes are designed to serve as blueprints for other classes to inherit from and provide concrete implementations for abstract methods.
In Python, abstract classes can be defined using the abc
module, which stands for Abstract Base Classes.
Example of an abstract class with an abstract method in Python:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
rectangle = Rectangle(5, 3)
print(rectangle.area()) # Output: 15
15
In this example:
Shape
is an abstract class that defines an abstract methodarea()
.Rectangle
is a subclass ofShape
that provides a concrete implementation for thearea()
method.We can instantiate objects of the
Rectangle
class and call thearea()
method, but we cannot instantiate objects of theShape
class directly because itâs an abstract class.
Empty Class:#
An empty class is a class that doesnât contain any attributes or methods. It serves as a placeholder or a base for future development. Empty classes are sometimes used as markers or placeholders in code, and they can be extended later to add functionality.
Example of an empty class in Python:
class Placeholder:
pass
This class doesnât contain any attributes or methods. It can be instantiated, but it doesnât have any behavior associated with it unless additional attributes or methods are added.
Data Class:#
A data class is a class that primarily holds data and doesnât contain any behavior (i.e., methods). Data classes are used to create simple data structures to store and manipulate data. In Python, the dataclasses
module provides a decorator and functions for automatically adding special methods such as __init__()
, __repr__()
, __eq__()
, and __hash__()
to a class based on its attributes.
Example of a data class in Python:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(3, 5)
print(p1) # Output: Point(x=3, y=5)
Point(x=3, y=5)
In this example, the @dataclass
decorator automatically generates an __init__()
, __repr__()
, __eq__()
, and __hash__()
methods based on the attributes x
and y
. This simplifies the creation of simple data-holding classes in Python.
Keyword Arguments#
Keyword arguments are a feature in Python that allows you to pass arguments to a function using their corresponding parameter names. When calling a function, you can specify the values for parameters by explicitly mentioning the parameter names along with their values. This provides greater flexibility and clarity in function calls, especially when dealing with functions that have many parameters or default parameter values.
Hereâs how keyword arguments work in Python:
def greet(name, message):
print(f"Hello, {name}! {message}")
# Using positional arguments
greet("Ali", "How are you?") # Output: Hello, Ali! How are you?
# Using keyword arguments
greet(message="How are you?", name="Bob") # Output: Hello, Bob! How are you?
Hello, Ali! How are you?
Hello, Bob! How are you?
In this example:
The
greet()
function accepts two parameters:name
andmessage
.When calling the function with positional arguments, the arguments are matched based on their order.
When calling the function with keyword arguments, the arguments are matched based on their names, regardless of their order.
Keyword arguments allow you to specify arguments in any order and skip optional arguments by providing only the values you want to change.