30.3 Abstract Properties and Class Methods
Abstract base classes (ABCs) serve as blueprints for other classes, enforcing a contract that derived classes must implement specific methods and properties. While abstract methods are the most common mechanism for this, the abc module also provides decorators to define abstract properties and class methods, ensuring a consistent interface across hierarchies that includes more than just instance methods.
Defining Abstract Properties
An abstract property mandates that concrete subclasses must provide an implementation for that property. This is achieved using the @abstractmethod decorator in combination with the built-in @property decorator. The order of these decorators is critical: @abstractmethod must be the outermost decorator. This is because decorators are applied from the bottom up. The @property decorator creates a descriptor object, and the @abstractmethod then wraps that descriptor, marking it as abstract and ensuring the ABC’s __init__ method cannot complete unless it is overridden.
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def description(self) -> str:
"""A read-only property providing a description of the vehicle."""
pass
@property
@abstractmethod
def fuel_capacity(self):
pass
@fuel_capacity.setter
@abstractmethod
def fuel_capacity(self, value):
pass
class Car(Vehicle):
def __init__(self, make, model):
self._make = make
self._model = model
self._fuel = 0
@property
def description(self) -> str:
return f"{self._make} {self._model}"
@property
def fuel_capacity(self):
return 50 # 50 liters
@fuel_capacity.setter
def fuel_capacity(self, value):
# This might raise an error or log a warning in a real scenario
print(f"Cannot change fuel capacity from 50L to {value}L")
# Attempting to instantiate without implementing all abstract properties would raise a TypeError.
my_car = Car("Toyota", "Corolla")
print(my_car.description) # Output: Toyota Corolla
print(my_car.fuel_capacity) # Output: 50
my_car.fuel_capacity = 60 # Output: Cannot change fuel capacity from 50L to 60L
In this example, Vehicle defines two abstract properties. description is a read-only property, while fuel_capacity is defined with both a getter and a setter, making it read-write. The Car subclass provides concrete implementations for both. If a subclass failed to implement description, instantiation would fail.
Defining Abstract Class Methods and Static Methods
Abstract class methods and static methods ensure that subclasses implement methods that are intended to be called on the class itself, rather than on instances. Common use cases include alternative constructors (class methods) or utility functions related to the class (static methods). The @abstractmethod decorator is combined with @classmethod or @staticmethod, again with @abstractmethod placed as the outermost decorator.
from abc import ABC, abstractmethod
from datetime import date
class DatabaseConnector(ABC):
@classmethod
@abstractmethod
def create_from_config(cls, config_file_path: str):
"""An abstract class method acting as an alternative constructor."""
pass
@staticmethod
@abstractmethod
def get_default_connection_string() -> str:
"""An abstract static method to provide a default configuration."""
pass
@abstractmethod
def connect(self):
pass
class PostgresConnector(DatabaseConnector):
def __init__(self, connection_string):
self.connection_string = connection_string
@classmethod
def create_from_config(cls, config_file_path: str):
# Simulate reading a connection string from a file
dummy_connection_string = f"postgresql://user:pass@localhost:5432/db?config={config_file_path}"
return cls(dummy_connection_string)
@staticmethod
def get_default_connection_string() -> str:
return "postgresql://user:pass@localhost:5432/test_db"
def connect(self):
print(f"Connecting to Postgres with: {self.connection_string}")
# Using the abstract class methods
default_conn_str = PostgresConnector.get_default_connection_string()
print(f"Default connection string: {default_conn_str}")
config_connector = PostgresConnector.create_from_config("prod_config.yml")
config_connector.connect()
This ABC defines an interface for database connectors that includes a class method for instance creation from a config file and a static method for retrieving a default configuration. The PostgresConnector provides the specific implementations, adhering to the contract.
Common Pitfalls and Best Practices
A frequent mistake is incorrect decorator ordering. Placing @property or @classmethod above @abstractmethod will not create a valid abstract member and will likely break the ABC’s enforcement mechanism. Always ensure @abstractmethod is the topmost decorator.
Another subtle pitfall involves the mutable default argument problem within abstract methods. While the abstract method itself only has a pass, if you define a method signature with a mutable default argument (e.g., def method(self, arg=[]):), this default list is created once at the time of the function definition. Any concrete subclass that uses this default will share the same mutable object across calls, leading to unexpected behavior. It is a best practice to avoid mutable defaults in abstract method signatures; use None instead and create the mutable object inside the method if needed.
When designing an ABC, be deliberate about what should be an abstract property versus an abstract method. Use properties for access to data that should look like an attribute (e.g., name, fuel_level) and use methods for actions (e.g., save(), calculate_range()). Overusing abstract properties can lead to an anemic domain model where complex logic is awkwardly stuffed into property getters. The combination of abstract read-write properties with setters provides a clear way to define required attributes while potentially encapsulating validation logic within the setter of the concrete subclass.