Class Inheritance in Python
In object-oriented programming, class inheritance is a powerful mechanism that allows you to create new classes(subclass) based on existing ones(superclass). This concept forms the foundation for building more complex and structured software systems. Inheritance enables you to model relationships between classes, reuse code, and establish a hierarchy of classes with shared attributes and behaviors.
Creating Subclass
We're going to create a Vehicle
class and then create a Car
class that inherits from it.
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start_engine(self):
print(f"The {self.model}'s engine is now running.")
def stop_engine(self):
print(f"The {self.model}'s engine has stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, doors):
super().__init__(make, model, year)
self.doors = doors
def lock_doors(self):
print(f"All {self.doors} doors are now locked.")
def unlock_doors(self):
print(f"All {self.doors} doors are now unlocked.")
Key Concepts in Inheritance
-
Superclass (Base Class): In the above example,
Vehicle
is the superclass. It serves as the blueprint for common attributes and methods that will be inherited by subclasses. -
Subclass (Derived Class):
Car
is a subclass. It inherits attributes and methods from theVehicle
superclass while also having the ability to define their own unique attributes and methods. TheCar
class inherits thestart_engine
andstop_engine
methods fromVehicle
, and it also defines its own methodslock_doors
andunlock_doors
. -
super
function: In class inheritance, thesuper()
function plays a crucial role in facilitating communication between subclasses and superclasses. It allows subclasses to access and utilize methods and attributes from their superclasses. Thesuper().__init__(make, model, year)
line is used to call the initializer of the parent class, which sets themake
,model
, andyear
attributes. TheCar
class also defines an additional attributedoors
. -
"Is-a" Relationship: In Python class inheritance, the "is-a" relationship refers to the relationship between a base class and a derived class. A derived class is a specialized version of the base class. In other words, a derived class "is-a" base class. This relationship highlights the hierarchy of classes and the specialization of subclasses.
Using the Subclass
Now that we have the Vehicle
and Car
classes, let's create instances and see how inheritance works:
# Creating an instance of Car
my_car = Car("Toyota", "Corolla", 2020, 4)
# Calling methods from the parent class
my_car.start_engine() # Output: The Corolla's engine is now running.
my_car.stop_engine() # Output: The Corolla's engine has stopped.
# Calling methods from the child class
my_car.lock_doors() # Output: All 4 doors are now locked.
my_car.unlock_doors() # Output: All 4 doors are now unlocked.
-
my_car = Car("Toyota", "Corolla", 2020, 4)
This line creates an instance of theCar
class namedmy_car
. TheCar
class requires four arguments to instantiate:make
,model
,year
, anddoors
. In this case, we're creating aCar
object that represents a 2020 Toyota Corolla with 4 doors. -
my_car.start_engine()
andmy_car.stop_engine()
These lines calls thestart_engine
andstop_engine
methods on themy_car
object. Because theCar
class inherits from theVehicle
class, it has access to these methods defined in theVehicle
class. This method prints a message indicating that the car's engine is now running. -
my_car.lock_doors()
&my_car.unlock_doors()
These lines calls thelock_doors
andunlock_doors
methods on themy_car
object. These method are defined in theCar
subclass and unique to the subclass
Benefits of Class Inheritance
-
Code Reusability: Inheritance promotes code reuse by allowing subclasses to inherit attributes and methods from a common superclass. This reduces redundancy and ensures a more efficient development process.
-
Modularity: Superclasses encapsulate shared behaviors and attributes, creating a modular code structure. Subclasses can then focus on specific details while relying on the inherited behavior.
-
Customization: Subclasses can customize inherited methods by overriding them. This enables subclasses to provide their own implementation of behaviors while maintaining a consistent interface.
Method Overriding
Method overriding is a key part of object-oriented programming that allows a subclass to provide a different implementation of a method that is already defined in its superclass. This mechanism enables specialized behavior in subclasses while adhering to the same method signature.
Here's an example:
class Animal:
def make_sound(self):
print("The animal makes a sound")
class Dog(Animal):
def make_sound(self):
print("The dog barks")
# Create instances of Animal and Dog
a = Animal()
d = Dog()
# Call make_sound method
a.make_sound() # Output: The animal makes a sound
d.make_sound() # Output: The dog barks
In this example, Animal
is the superclass and Dog
is the subclass. Both classes have a method named make_sound
. When make_sound
is called on an instance of Animal
, it prints "The animal makes a sound". But when make_sound
is called on an instance of Dog
, it prints "The dog barks". This is because Dog
has overridden the make_sound
method of Animal
.
More on super()
Function
The super()
function provides a way for subclasses to invoke methods and access attributes of their superclass. It is used to call methods in the superclass and can be particularly useful when you want to extend or customize the behavior of the superclass's method.
Sure, let's dive deeper into the super()
function in Python.
1. Extending a Constructor with super()
:
The super()
function is often used in the constructor method (__init__
) of a subclass to ensure that the initialization code of the superclass is executed before the code of the subclass. Here's an example:
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call the constructor of Animal
self.breed = breed
# Create an instance of Dog
d = Dog("Fido", "Labrador")
print(d.name) # Output: Fido
print(d.species) # Output: Dog
print(d.breed) # Output: Labrador
In this example, Dog
is a subclass of Animal
. When an instance of Dog
is created, super().__init__(name, "Dog")
is used to call the constructor of Animal
to set the name
and species
attributes. Then, Dog
's constructor continues to set the breed
attribute.
2. Using super()
for Method Invocation:
The super()
function can also be used to call other methods from the superclass that have been overridden in the subclass. Here's an example:
class Animal:
def make_sound(self):
print("The animal makes a sound")
class Dog(Animal):
def make_sound(self):
super().make_sound() # Call the make_sound method of Animal
print("The dog barks")
# Create an instance of Dog
d = Dog()
d.make_sound()
# Output:
# The animal makes a sound
# The dog barks
In this example, Dog
is a subclass of Animal
and both classes have a make_sound
method. When make_sound
is called on a Dog
instance, super().make_sound()
is used to call the make_sound
method of Animal
before the rest of Dog
's make_sound
method is executed. Compare this with method overriding implementation above to understand the difference better.
Multiple Inheritance
Multiple inheritance is a feature in object-oriented programming that allows a class to inherit attributes and methods from multiple superclasses. This concept enables the creation of complex relationships and behaviors by combining the attributes and methods of different classes.
Here's a basic example of multiple inheritance:
class Parent1:
def method1(self):
print("This is from Parent1")
class Parent2:
def method2(self):
print("This is from Parent2")
class Child(Parent1, Parent2):
pass
# Create an instance of Child
child = Child()
# Call methods from Parent1 and Parent2
child.method1() # Output: This is from Parent1
child.method2() # Output: This is from Parent2
In this example, Child
inherits from both Parent1
and Parent2
, and it has access to method1
from Parent1
and method2
from Parent2
.
Method Resolution Order (MRO)
However, what happens if both parent classes have a method of the same name? Python resolves this using the Method Resolution Order (MRO), which is determined by the C3 linearization algorithm. The MRO dictates the order in which Python will look for methods in superclass hierarchies.
Here's an example:
class Parent1:
def method(self):
print("This is from Parent1")
class Parent2:
def method(self):
print("This is from Parent2")
class Child(Parent1, Parent2):
pass
# Create an instance of Child
child = Child()
# Call the method
child.method() # Output: This is from Parent1
In this case, Parent1
's method
is called, because Parent1
appears before Parent2
in the MRO of Child
. You can view the MRO using the mro
method:
print(Child.mro()) # Output: [<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class 'object'>]
This shows that Python will look for methods in the order: Child
, Parent1
, Parent2
, object
.
More on Method Resolution Order (MRO)
Method Resolution Order (MRO) is the order in which Python looks for a method in a hierarchy of classes. Mainly, it comes into play when dealing with multiple inheritance, as it can get pretty complicated when several parents have the same method and you need to figure out which one gets called.
Here is an example:
class A:
def process(self):
print('A process()')
class B(A):
pass
class C(A):
def process(self):
print('C process()')
class D(B, C):
pass
obj = D()
obj.process()
# Output: C process()
In this example, both A
and C
classes have a method named process
. D
is a subclass of B
and C
. When process
is called on an instance of D
, Python needs to decide whether A
's process
or C
's process
should be called.
Python uses the C3 linearization or MRO algorithm to make this decision. The MRO order for class D
is D -> B -> C -> A -> object
. So, Python first looks in class D
. If D
doesn't have the method, Python looks in B
, then in C
, then in A
, and finally in object
(which is the ultimate base class of all classes in Python). In this case, Python finds the process
method in C
first, so C process()
is printed.
You can view the MRO of a class using the mro
method:
print(D.mro())
# Output: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
Comprehensive Example
Let's explore a more practical example to demonstrate class inheritance, accessing superclass methods, method overriding by a subclass, and using the super()
function. In this example, we'll model a simple banking system with different types of accounts.
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
else:
print("Insufficient balance")
def display_balance(self):
print(f"Account {self.account_number}: Balance = ${self.balance:.2f}")
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance, interest_rate):
super().__init__(account_number, balance) # Call superclass constructor
self.interest_rate = interest_rate
def calculate_interest(self):
return self.balance * self.interest_rate
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance, overdraft_limit):
super().__init__(account_number, balance) # Call superclass constructor
self.overdraft_limit = overdraft_limit
def withdraw(self, amount): # Method overriding
if self.balance + self.overdraft_limit >= amount:
self.balance -= amount
else:
print("Exceeds overdraft limit")
# Creating instances
savings = SavingsAccount("12345", 1000, 0.05)
checking = CheckingAccount("67890", 500, 200)
# Using methods on savings account
savings.deposit(500)
savings.withdraw(200)
savings.display_balance()
print("Interest:", savings.calculate_interest())
# Using methods on checking accounts
checking.withdraw(100)
checking.display_balance()
checking.withdraw(500)
checking.display_balance()
checking.withdraw(500)
checking.display_balance()
Account 12345: Balance = $1400.00
Interest: 70.0
Account 67890: Balance = $400.00
Account 67890: Balance = $-100.00
Exceeds overdraft limit
Account 67890: Balance = $-100.00
In this example:
- We define a
BankAccount
class as the superclass with basic methods to deposit, withdraw, and display balance. - The
SavingsAccount
class inherits fromBankAccount
and introduces aninterest_rate
attribute. It overrides thedeposit
method from the superclass to calculate interest. - The
CheckingAccount
class inherits fromBankAccount
and introduces anoverdraft_limit
attribute. It overrides thewithdraw
method from the superclass to allow overdrafts within the limit. - We create instances of
SavingsAccount
andCheckingAccount
to showcase the various aspects of class inheritance:- The
SavingsAccount
instance utilizes the inherited methods and attributes, as well as the overriddencalculate_interest
method. - The
CheckingAccount
instance demonstrates method overriding by customizing thewithdraw
method to accommodate overdrafts within the limit.
- The