When it comes to writing reusable and extensible code in Python, abstract classes are an essential tool in your arsenal. Abstract classes define a set of methods that a subclass must implement, but it doesn’t provide an implementation for those methods itself. This feature makes them a powerful way to enforce certain behavior while also allowing for customization and flexibility in your code. In this post, we’ll explore what abstract classes are, their use cases, and some patterns you can use with them.
What are Abstract Classes?
An abstract class is a special kind of class that cannot be instantiated directly. Instead, it is meant to be subclassed, and the subclass must provide implementations for its abstract methods. Abstract classes are a way to define a blueprint or a template for a set of related classes. They allow you to define a set of methods that must be implemented in each subclass, without actually implementing those methods in the abstract class itself.
In Python, you can define an abstract class by using the abc
module. This module provides the ABC
class, which you can subclass to create your own abstract classes. The ABC
class doesn’t do anything on its own, but it marks the class as abstract and allows you to define abstract methods.
Here’s an example:
from abc import ABC, abstractmethod class Vehicle(ABC): @abstractmethod def start(self): pass @abstractmethod def stop(self): pass
In this example, we define an abstract class Vehicle
with two abstract methods: start
and stop
. These methods don’t have an implementation, but any subclass of Vehicle
must implement them.
Use Cases for Abstract Classes
Creating a plugin system
If you’re building a system where you want other developers to be able to create plugins, you can define an abstract class that they must subclass to create a plugin. This ensures that all plugins conform to a common interface and have the same behavior.
For example, if you’re building a text editor and you want to allow developers to create new file formats, you can define an abstract class FileFormat
with methods like load
and save
. Then, any developer who wants to create a new file format can subclass FileFormat
and implement those methods.
from abc import ABC, abstractmethod class FileFormat(ABC): @abstractmethod def load(self, file_path): pass @abstractmethod def save(self, file_path): pass class JSONFile(FileFormat): def load(self, file_path): # load the file as JSON pass def save(self, file_path): # save the file as JSON pass class CSVFile(FileFormat): def load(self, file_path): # load the file as CSV pass def save(self, file_path): # save the file as CSV pass
In this example, we define an abstract class FileFormat
that has two abstract methods: load
and save
. We then create two subclasses, JSONFile
and CSVFile
, which implement those methods. Any other developer who wants to create a new file format can also subclass FileFormat
and implement the load
and save
methods.
Defining an interface for a library
When building a library, you may want to define an interface that other developers must use to interact with your library. This interface can be defined as an abstract class, which other developers can subclass to create their own classes that interact with your library.
For example, if you’re building a database library, you can define an abstract class Database
with methods like connect
, execute_query
, and close
. Then, other developers can create their own database classes that subclass Database
and provide implementations for those methods.
from abc import ABC, abstractmethod class Database(ABC): @abstractmethod def connect(self): pass @abstractmethod def execute_query(self, query): pass @abstractmethod def close(self): pass class MySQLDatabase(Database): def connect(self): # connect to a MySQL database pass def execute_query(self, query): # execute a query on the database pass def close(self): # close the database connection pass class PostgreSQLDatabase(Database): def connect(self): # connect to a PostgreSQL database pass def execute_query(self, query): # execute a query on the database pass def close(self): # close the database connection pass
In this example, we define an abstract class Database
that has three abstract methods: connect
, execute_query
, and close
. We then create two subclasses, MySQLDatabase
and PostgreSQLDatabase
, which implement those methods. Other developers can create their own database classes that subclass Database
and provide implementations for those methods.
Ensuring consistency in a hierarchy of classes
If you’re building a hierarchy of related classes, you can use an abstract class to ensure that all the classes in the hierarchy have the same interface. This can help you avoid bugs and make your code more maintainable.
For example, if you’re building a game with different types of enemies, you can define an abstract class Enemy
with methods like attack
and take_damage
. Then, you can create different subclasses of Enemy
for each type of enemy in your game.
from abc import ABC, abstractmethod class Enemy(ABC): @abstractmethod def attack(self): pass @abstractmethod def take_damage(self, damage): pass class Goblin(Enemy): def attack(self): # implement the Goblin's attack pass def take_damage(self, damage): # implement the Goblin's take_damage method pass class Ogre(Enemy): def attack(self): # implement the Ogre's attack pass def take_damage(self, damage): # implement the Ogre's take_damage method pass
In this example, we define an abstract class Enemy
that has two abstract methods: attack
and take_damage
. We then create two subclasses, Goblin
and Ogre
, which implement those methods. Any other subclasses that we create for different types of enemies in the game will also have to implement those methods, ensuring consistency in the hierarchy of classes.
Patterns for Abstract Classes
Template Method Pattern
The Template Method Pattern is a design pattern that uses an abstract class to define a template for an algorithm, with some steps that are defined by the abstract class and some steps that must be defined by a subclass.
One use case for the Template Method Pattern is when you have a consistent algorithm that needs to be implemented across multiple subclasses, with some variations. For example, imagine that you’re building a game with different types of enemies, and each type of enemy has a different way of attacking. You could define an abstract class Enemy with a template method called attack
, which defines the overall steps for an attack, such as selecting a target, positioning, and attacking the target. The individual steps would be implemented as abstract methods that must be defined by each enemy subclass.
Here’s an example implementation of the Template Method Pattern for the Enemy use case in we talked about earlier (before and after applying the pattern):
class Enemy: def __init__(self, name, health, damage): self.name = name self.health = health self.damage = damage def attack(self, target): # Attack the target def move(self): # Move to a new location class Goblin(Enemy): def take_turn(self, target): self.attack(target) self.move() class Orc(Enemy): def take_turn(self, target): self.move() self.attack(target)
In this example, the Goblin attacks before moving, while the Orc moves before attacking. This can lead to inconsistent gameplay and make it difficult to balance the game. To ensure consistent algorithms across related classes, we should use the Template Method pattern.
Applying Template Method pattern:
from abc import ABC, abstractmethod class Enemy(ABC): def __init__(self, name, health, damage): self.name = name self.health = health self.damage = damage def take_turn(self, target): self.select_target(target) self.attack_target() self.move_to_new_location() @abstractmethod def select_target(self, target): pass @abstractmethod def attack_target(self): pass @abstractmethod def move_to_new_location(self): pass class Goblin(Enemy): def select_target(self, target): # Select the target def attack_target(self): # Attack the target def move_to_new_location(self): # Move to a new location class Orc(Enemy): def select_target(self, target): # Select the target def attack_target(self): # Attack the target def move_to_new_location(self): # Move to a new location
In this example, the Enemy class defines a template method called take_turn, which defines the overall steps for an enemy’s turn in the game. Each step is defined as an abstract method that must be implemented by each enemy subclass. This ensures that each enemy takes its turn in a consistent way, making it easier to balance the game and create a consistent player experience.
The Strategy Pattern
The Strategy Pattern is a design pattern that uses an abstract class to define a family of algorithms that can be selected and used dynamically at runtime.
Suppose you are building a payment system that can accept payments from multiple payment gateways like PayPal, Stripe, and Square. Each payment gateway has its own API and different requirements for making and processing payments. You want to build a flexible payment system that can switch between different payment gateways at runtime, without modifying the code.
In this scenario, you can use the Strategy Pattern. You can define an abstract class PaymentStrategy with an abstract method called process_payment
. Then you can create concrete classes that implement this method and provide the specific implementation for each payment gateway.
rom abc import ABC, abstractmethod class PaymentStrategy(ABC): @abstractmethod def process_payment(self, amount): pass class PayPalStrategy(PaymentStrategy): def process_payment(self, amount): # Code to process payment via PayPal API print(f"Processing payment of ${amount} via PayPal...") class StripeStrategy(PaymentStrategy): def process_payment(self, amount): # Code to process payment via Stripe API print(f"Processing payment of ${amount} via Stripe...") class SquareStrategy(PaymentStrategy): def process_payment(self, amount): # Code to process payment via Square API print(f"Processing payment of ${amount} via Square...") class PaymentContext: def __init__(self, strategy: PaymentStrategy): self.strategy = strategy def set_strategy(self, strategy: PaymentStrategy): self.strategy = strategy def process_payment(self, amount): self.strategy.process_payment(amount) # Example usage paypal = PayPalStrategy() stripe = StripeStrategy() square = SquareStrategy() payment = PaymentContext(paypal) payment.process_payment(100) payment.set_strategy(stripe) payment.process_payment(200) payment.set_strategy(square) payment.process_payment(300)
In this example, PaymentStrategy
is the abstract class that defines the strategy interface. PayPalStrategy
, StripeStrategy
, and SquareStrategy
are the concrete classes that implement this interface with their specific payment processing logic.
The PaymentContext
class is the context class that holds the current strategy and provides a public method process_payment
to initiate the payment processing. The set_strategy
method allows you to switch between different payment gateways at runtime.
You can see that the process_payment
method of each concrete class contains specific code to process the payment via the corresponding API. The context class does not need to know about these specific implementation details. It just delegates the payment processing to the current strategy object, which can be changed dynamically.
In this post, we’ve explored the concept of abstract classes in Python. We learned that abstract classes are classes that cannot be instantiated and that are intended to be subclassed by concrete classes. We also discussed some use cases for abstract classes, such as defining interfaces and implementing common behaviors across related classes.
We then delved into two common design patterns that use abstract classes: the template method pattern and the strategy pattern. The template method pattern allows you to define a skeleton of an algorithm in an abstract class, with specific steps implemented in concrete subclasses. The strategy pattern allows you to define a family of algorithms, encapsulate each one as a separate class, and make them interchangeable at runtime.
By using abstract classes and design patterns effectively, you can write more modular, maintainable, and extensible code.
If you’re interested in learning more about abstract classes in Python, you can check out the official Python documentation on abstract base classes: https://docs.python.org/3/library/abc.html. You may also want to explore other design patterns that use abstract classes, such as the factory method pattern and the iterator pattern.