In the previous article, I introduced you to the object-oriented programming paradigm and explained the fundamental concepts of classes and objects. You saw how they encapsulate the attributes and methods within them and how each object carries its own set of these attributes. Now, we are all set to get started with two more compelling concepts of OOP, i.e., inheritance and polymorphism. Inheritance and polymorphism enable us to achieve remarkable code reusability, extend functionality without modifying the existing code, and provide us with ways to handle different object types. By the end of this lesson, you will be able to build class hierarchies and make objects behave differently with similar method calls.
Inheritance
The term inheritance in programming means deriving attributes and methods from one class and creating a new class with its own set of attributes and behaviors. Just like in real life, we inherit our hair, eyes, nose, and other properties from our parents, a derived class inherits attributes from its parent class.
- The new class is generally referred to as a subclass, derived class, or child class.
- The class from which another class is derived is called the superclass, base class, or parent class.
- Inheritance establishes an "is-a" relationship among classes.
Benefits of Inheritance
- If similar functionality is needed in two classes, it is better to inherit one class from another rather than writing similar code twice (reusability).
- To have variants, you may want to add more functionality or new features to a class. Inheritance is the best way to achieve it (extensibility).
- Changes made in the parent class are automatically applied to the child classes (increased maintainability).
Defining a Subclass
Syntax:
class child_class_name(parent_class):
attributes
methods
Example:
class Animal: # Parent class
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " makes a sound."
class Dog(Animal): # Dog is a subclass of Animal class
def __init__(self, name, breed):
# Call the parent's (Animal's) __init__ method
super().__init__(name)
self.breed = breed # Add a new attribute specific to Dog
def bark(self): # Add a new method specific to Dog
return self.name + " barks loudly!"
# Create instances
animal = Animal("Animal")
dog = Dog("Buddy", "Golden Retriever")
print(animal.speak()) # Output: Generic Animal makes a sound.
print(dog.speak()) # Output: Buddy makes a sound. (Inherited from Animal)
print(dog.bark()) # Output: Buddy barks loudly! (Dog's own method)
print(dog.name) # Output: Buddy (Inherited attribute)
Output:
Animal makes a sound.
Buddy makes a sound.
Buddy barks loudly!
Buddy
Polymorphism
The term polymorphism is derived from a Greek word that means 'many forms.' In programming, when you want a method to behave differently based on the parameters passed or the object they are being called with, we use polymorphism instead of creating multiple functions.
We achieve polymorphism in two ways:
- Function overloading.
- Function overriding.
Function Overloading
When there are multiple definitions of the same function with different numbers of parameters or return values, we call it function overloading. It is useful in situations where you want a function to perform different actions based on the values passed by the user.
For example, previously, we wrote an add function that took two parameters. What if the user wants to add three or four numbers? This is the type of situation where we use function overloading. Instead of defining three different functions for each type of addition, we use the same name for all functions and define different numbers of parameters in them.
Unfortunately, Python does not support method overloading directly. However, you can achieve that with alternative mechanisms like using variable-length arguments.
def add(*numbers): # Accepts variable length arguments
total = 0
for number in numbers:
total += number
return total
print(add(2,3,4)) # returns 9
print(add(1,2)) #returns 3
print(add(10, 20, 30, 40)) # returns 100
Function Overriding
When an inherited class has its own definition of a method defined inside the parent class, it is referred to as method overriding. Whenever an overridden method is called with the child's object, it always behaves as defined within the child class.
To understand this, let's override the speak()
method in the Dog
class:
class Animal: # Parent class
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " makes a sound."
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def speak(self): # Overriding the speak method from Animal
return self.name + " says Woof!"
def bark(self):
return self.name + " barks loudly!"
animal = Animal("Animal")
print(animal.speak()) # Calls the parent class method
dog = Dog("Max", "German Shepherd")
print(dog.speak()) # Calls the child class method
Output:
Animal makes a sound.
Max says Woof!
We have now unveiled two more fundamental concepts of OOP and are ready to dive deep into the concepts of encapsulation and abstraction. We'll discuss those in the next article. Till then, keep practicing.