Skip to content

Real-World Python: Inheritance and Polymorphism for Better Code Reuse

Updated: at 04:56 AM

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:

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:

  1. Inheritance - child classes inherit and override parent methods
  2. 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:

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!