Inheritance and polymorphism are two fundamental concepts in object-oriented programming that allow for greater code reuse and extensibility. This comprehensive guide examines practical applications of inheritance and polymorphism in Python programming, with example code snippets and explanations. Whether you are a beginner or an experienced Python developer, this article will illustrate how leveraging inheritance and polymorphism can help you write more efficient, maintainable code.
Introduction
Inheritance and polymorphism in Python enable you to reuse parent class code in child classes, overriding or extending its capabilities according to your needs. This promotes the DRY (Don’t Repeat Yourself) principle, reduces duplication, and allows centralized changes. Polymorphism gives different objects a common interface for code generality.
This guide will cover the following topics:
- Inheritance for subclass reuse
- Overriding parent methods
- Extending superclass functionality
- Abstract base classes and interfaces
- Polymorphism for flexible code
- Duck typing philosophy
- Real-world examples like GUI, math ops, etc.
By the end, you will have a clear understanding of how to utilize inheritance and polymorphism to refactor code for maximum reusability and extensibility.
Inheritance in Python
Inheritance allows a new child class to inherit attributes and methods from a parent class. This enables code reuse since you can leverage the parent’s implementation while customizing the child class.
Here’s an example parent class:
class Vehicle:
def __init__(self, make, color):
self.make = make
self.color = color
def drive(self):
print(f"The {self.color} {self.make} is being driven")
To inherit from Vehicle, we create a child class that passes Vehicle as a parameter to the superclass:
class Car(Vehicle):
def __init__(self, make, color, max_speed):
super().__init__(make, color)
self.max_speed = max_speed
Now Car instances inherit the drive()
method from the parent Vehicle class:
my_car = Car('Toyota', 'grey', 200)
my_car.drive()
# Output: The grey Toyota is being driven
This allows code reuse and the ability to share common logic across classes.
Overriding Methods
Inherited methods can be overridden in the child class to specialize their implementation:
class ElectricCar(Vehicle):
def __init__(self, make, color, battery_range):
super().__init__(make, color)
self.battery_range = battery_range
def drive(self):
print(f"The {self.color} {self.make} glides silently")
Now ElectricCar has its own unique drive()
behavior:
my_tesla = ElectricCar('Tesla', 'black', 300)
my_tesla.drive()
# Output: The black Tesla glides silently
Overriding methods is useful for customizing functionality while reusing the parent’s code.
Extending Functionality
In addition to overriding, child classes can extend the parent’s capabilities by invoking the superclass method:
class HybridCar(Vehicle):
def __init__(self, make, color, battery_range):
super().__init__(make, color)
self.battery_range = battery_range
def drive(self):
print("Driving on electricity")
super().drive()
Now HybridCar supplements the inherited drive()
method with additional logic:
my_prius = HybridCar('Toyota', 'blue', 40)
my_prius.drive()
# Output:
# Driving on electricity
# The blue Toyota is being driven
This shows how child classes can build on top of the inherited parent functionality.
Abstract Base Classes
Abstract base classes (ABC) are reusable parent classes that cannot be instantiated directly. They define abstract methods that all concrete child classes must implement.
For example:
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def start(self):
pass
@abstractmethod
def stop(self):
pass
Any child class must now implement start()
and stop()
:
class Car(Vehicle):
def __init__(self, make, color):
self.make = make
self.color = color
def start(self):
print(f"Starting the {self.color} {self.make}")
def stop(self):
print(f"Stopping the {self.color} {self.make}")
This enforces a common interface so code relying on Vehicle stays robust even as new subclasses are introduced.
Interfaces for Polymorphism
Interfaces in Python define method signatures that implementing classes must follow. This is another technique for polymorphism.
For example:
class VehicleInterface:
def start(self):
pass
def stop(self):
pass
def accelerate(self):
pass
Multiple diverse classes can implement this:
class Car(VehicleInterface):
def start(self):
print("Starting car")
def stop(self):
print("Stopping car")
def accelerate(self):
print("Accelerating car")
class Motorcycle(VehicleInterface):
def start(self):
print("Starting motorcycle")
def stop(self):
print("Stopping motorcycle")
def accelerate(self):
print("Accelerating motorcycle")
Now CAR and Motorcycle become interchangeable as VehicleInterfaces:
vehicles = [Car(), Motorcycle()]
for vehicle in vehicles:
vehicle.start()
# Starting car
# Starting motorcycle
This shows how interfaces enable polymorphism in Python.
Polymorphism in Python
Polymorphism allows different object types to implement the same method or operator interface. This enables more generic, adaptable code.
Method Overloading
In statically typed languages like Java, the same method can be defined multiple times with different signatures. But in Python, polymorphism mainly involves:
- Inheritance - child classes inherit and override parent methods
- Duck typing - objects of different unrelated types implement a common method or operator
For example:
class Geometry:
def area(self):
raise NotImplementedError
class Square(Geometry):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Circle(Geometry):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * (self.radius ** 2)
Squares and Circles can both provide area implementations despite having different underlying data. This allows for more generalized code:
def get_area(shapes):
for shape in shapes:
print(shape.area())
squares = [Square(5), Square(10)]
circles = [Circle(3), Circle(7)]
get_area(squares + circles)
# 25
# 100
# 28.274333882308138
# 153.93804002589985
Instead of method overloading, in Python the emphasis is on sharing interfaces through inheritance and duck typing.
Duck Typing
Duck typing refers to checking for the presence of a given method or attribute on an object, rather than the type of object itself.
For example, both tuples and lists have an append()
method in Python:
tuple_data = (1, 2)
list_data = [3, 4]
for data in (tuple_data, list_data):
data.append(5)
print(tuple_data)
print(list_data)
# (1, 2, 5)
# [3, 4, 5]
The common append()
interface enables generic processing, despite tuples and lists being different types. This dynamic treatment of objects based on their interface rather than strict type is a key aspect of duck typing.
Real-World Examples
Let’s now look at some real-world examples that leverage inheritance and polymorphism in Python for better code reuse.
Graphical User Interface Apps
GUI toolkits like Tkinter provide base classes that can be inherited to build custom user interfaces:
from tkinter import Tk, Entry, Button
class App(Tk):
def __init__(self):
super().__init__()
self.title("My App")
self.entry = Entry(self)
self.button = Button(self, text="Submit", command=self.on_submit)
self.entry.pack()
self.button.pack()
def on_submit(self):
print("Submitted: " + self.entry.get())
if __name__ == "__main__":
app = App()
app.mainloop()
Here App
inherits from Tk
to get default windowing capabilities, which are then customized. The entry and button also inherit common UI functionality. This approach allows for polymorphic controls with reusable code.
Math Operations
Python’s built-in math
module uses ABCs and operator overloading for polymorphic math utilities:
from abc import ABC, abstractmethod
class Vector(ABC):
@abstractmethod
def __add__(self, other):
pass
@abstractmethod
def __sub__(self):
pass
class ListVector(Vector):
def __init__(self, data):
self.data = data
def __add__(self, other):
return ListVector([a + b for a, b in zip(self.data, other.data)])
def __sub__(self, other):
return ListVector([a - b for a, b in zip(self.data, other.data)])
class TupleVector(Vector):
def __init__(self, data):
self.data = data
def __add__(self, other):
return TupleVector(tuple(a + b for a, b in zip(self.data, other.data)))
def __sub__(self, other):
return TupleVector(tuple(a - b for a, b in zip(self.data, other.data)))
This allows code like:
lv = ListVector([1, 2, 3])
tv = TupleVector((3, 4, 5))
print(lv + tv) # ListVector([4, 6, 8])
print(lv - tv) # ListVector([-2, -2, -2])
To work generically for any child Vector type thanks to polymorphism.
Game Development
Game objects like enemies or powerups can inherit shared functionality from base classes:
class GameObject:
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
print(f"Drawing object at ({self.x}, {self.y})")
def update(self):
pass
class Enemy(GameObject):
def __init__(self, x, y):
super().__init__(x, y)
self.speed = 5
def update(self):
self.x += self.speed
class Powerup(GameObject):
def __init__(self, x, y):
super().__init__(x, y)
self.power = 100
def apply(self, player):
player.power += self.power
This allows for enemy and powerup reuse while still being customized via inheritance. The inherited draw()
and update()
methods also enable polymorphism during game loops:
enemies = [Enemy(1, 2), Enemy(3, 5)]
powerups = [Powerup(2, 1), Powerup(4, 3)]
for obj in enemies + powerups:
obj.draw()
obj.update()
Data Analysis
Libraries like Pandas use inheritance to enable polymorphic analysis of different data sources:
class DataSource(ABC):
@abstractmethod
def load_data(self):
pass
@abstractmethod
def process(self):
pass
class CSVFile(DataSource):
def load_data(self):
# Load CSV file
def process(self):
# Process and analyze CSV data
class SQLDatabase(DataSource):
def load_data(self):
# Connect and load SQL data
def process(self):
# Process and analyze SQL data
This facilitates generic handling of different data sources:
data_sources = [CSVFile(), SQLDatabase()]
for source in data_sources:
df = source.load_data()
source.process()
The shared interfaces enable flexible analysis workflows.
Conclusion
Inheritance and polymorphism are powerful concepts in Python that facilitate efficient code reuse and extensibility:
- Inheritance enables child classes to inherit parent attributes and override or extend methods as needed
- Abstract base classes and interfaces enforce standards across child classes
- Polymorphism allows classes of different types to share common interfaces
- Duck typing philosophy prioritizes common interfaces over strict types
Applied effectively, these techniques will make your Python code more DRY, maintainable and adaptable. The real-world examples demonstrate inheritance and polymorphism in action for GUI apps, math utilities, games, data analysis, and more.
I hope this guide provided a comprehensive overview of how to leverage inheritance and polymorphism to write better Python code. Let me know if you have any other questions!