Virtual Subclasses (register)¶
The register() method allows classes to be registered as virtual subclasses of an ABC without inheriting from it. This enables duck typing with formal guarantees.
Basic Register Usage¶
from abc import ABC, abstractmethod
class DataStore(ABC):
@abstractmethod
def save(self, key, value):
pass
@abstractmethod
def load(self, key):
pass
# Register an existing class as virtual subclass
class FileStore:
def save(self, key, value):
# Implementation
pass
def load(self, key):
# Implementation
pass
DataStore.register(FileStore)
# Now FileStore is considered a subclass
print(isinstance(FileStore(), DataStore)) # True
print(issubclass(FileStore, DataStore)) # True
Practical Example: Multiple Storage Backends¶
from abc import ABC, abstractmethod
class Cache(ABC):
@abstractmethod
def get(self, key):
pass
@abstractmethod
def set(self, key, value):
pass
class RedisCache:
def __init__(self):
self.data = {}
def get(self, key):
return self.data.get(key)
def set(self, key, value):
self.data[key] = value
class MemcachedCache:
def __init__(self):
self.data = {}
def get(self, key):
return self.data.get(key)
def set(self, key, value):
self.data[key] = value
# Register both as Cache implementations
Cache.register(RedisCache)
Cache.register(MemcachedCache)
def use_cache(cache: Cache):
cache.set('key', 'value')
return cache.get('key')
# Works with either cache
redis = RedisCache()
memcached = MemcachedCache()
print(isinstance(redis, Cache)) # True
print(isinstance(memcached, Cache)) # True
print(use_cache(redis)) # 'value'
print(use_cache(memcached)) # 'value'
Chained Register¶
from abc import ABC, abstractmethod
class Serializer(ABC):
@abstractmethod
def serialize(self, obj):
pass
@abstractmethod
def deserialize(self, data):
pass
class JsonSerializer:
def serialize(self, obj):
import json
return json.dumps(obj)
def deserialize(self, data):
import json
return json.loads(data)
class PickleSerializer:
def serialize(self, obj):
import pickle
return pickle.dumps(obj)
def deserialize(self, data):
import pickle
return pickle.loads(data)
# Chain registrations
Serializer.register(JsonSerializer)
Serializer.register(PickleSerializer)
def process_data(serializer: Serializer, obj):
serialized = serializer.serialize(obj)
return serializer.deserialize(serialized)
data = {'name': 'Alice', 'age': 30}
js = JsonSerializer()
print(isinstance(js, Serializer)) # True
print(process_data(js, data)) # {'name': 'Alice', 'age': 30}
Subclass Checking¶
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
class ConsoleLogger:
def log(self, message):
print(message)
class FileLogger:
def log(self, message):
with open('log.txt', 'a') as f:
f.write(message + '
')
Logger.register(ConsoleLogger)
Logger.register(FileLogger)
# Check subclass relationship
print(issubclass(ConsoleLogger, Logger)) # True
print(issubclass(FileLogger, Logger)) # True
# Check instance relationship
console = ConsoleLogger()
print(isinstance(console, Logger)) # True
# Use in type checking
def get_logger(config) -> Logger:
if config.get('output') == 'console':
return ConsoleLogger()
else:
return FileLogger()
Virtual Subclass Benefits¶
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount):
pass
# Existing third-party class
class StripeProcessor:
def process(self, amount):
return f"Processing ${amount} with Stripe"
# Register without modifying the original class
PaymentProcessor.register(StripeProcessor)
# Now you can write functions expecting PaymentProcessor
def charge_user(processor: PaymentProcessor, amount: float):
'''Works with any registered PaymentProcessor'''
return processor.process(amount)
stripe = StripeProcessor()
print(charge_user(stripe, 100)) # Works!
Limitations of Virtual Subclasses¶
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self):
pass
class Circle:
def draw(self):
print("Drawing circle")
# Register Circle as virtual Drawable
Drawable.register(Circle)
# This doesn't enforce the interface!
class BadCircle:
pass
# Still creates virtual subclass, but doesn't implement interface
Drawable.register(BadCircle)
# isinstance checks pass but interface isn't guaranteed
print(isinstance(BadCircle(), Drawable)) # True!
bad = BadCircle()
# bad.draw() # AttributeError - draw method doesn't exist!
Best Practices¶
- Use register() for adapting existing classes
- Ensure registered classes actually implement the interface
- Document the expected interface clearly
- Consider inheritance for new classes (enforces interface)
- Use ABC with abstractmethod for strict enforcement
- Register for duck typing compatibility
Runnable Example: factory_pattern_with_abc_example.py¶
"""
Factory Pattern with Abstract Base Classes
Combines ABCs for defining contracts with the Factory Pattern
for creating objects. This is a payroll system where different
employee types have different salary calculation methods.
Topics covered:
- Abstract base classes (ABCMeta, abstractmethod)
- Factory pattern (centralized object creation)
- Polymorphism (same interface, different behavior)
- Static methods in factory classes
Based on concepts from Python-100-Days examples 12-13 and ch06/abc materials.
"""
from abc import ABCMeta, abstractmethod
# =============================================================================
# Example 1: Abstract Employee Class
# =============================================================================
class Employee(metaclass=ABCMeta):
"""Abstract base class defining the employee contract.
All employee types must implement get_salary().
This ensures polymorphic salary calculation.
"""
def __init__(self, name: str):
self.name = name
@abstractmethod
def get_salary(self) -> float:
"""Calculate monthly salary. Must be implemented by subclasses."""
pass
def __str__(self):
return f"{self.__class__.__name__}('{self.name}')"
def __repr__(self):
return self.__str__()
# =============================================================================
# Example 2: Concrete Employee Subclasses
# =============================================================================
class Manager(Employee):
"""Department manager with fixed salary."""
def __init__(self, name: str, monthly_salary: float = 15000.0):
super().__init__(name)
self.monthly_salary = monthly_salary
def get_salary(self) -> float:
return self.monthly_salary
class Programmer(Employee):
"""Programmer paid by hours worked."""
def __init__(self, name: str, hourly_rate: float = 200.0,
hours_worked: int = 0):
super().__init__(name)
self.hourly_rate = hourly_rate
self.hours_worked = hours_worked
def get_salary(self) -> float:
return self.hourly_rate * self.hours_worked
class Salesperson(Employee):
"""Salesperson with base salary plus commission."""
def __init__(self, name: str, base_salary: float = 1800.0,
sales: float = 0.0, commission_rate: float = 0.05):
super().__init__(name)
self.base_salary = base_salary
self.sales = sales
self.commission_rate = commission_rate
def get_salary(self) -> float:
return self.base_salary + self.sales * self.commission_rate
# =============================================================================
# Example 3: Factory for Employee Creation
# =============================================================================
class EmployeeFactory:
"""Factory class for creating employees.
The Factory Pattern decouples object creation from usage.
Client code doesn't need to know the specific class - just
the type code. This makes it easy to add new employee types.
"""
_registry = {
'M': Manager,
'P': Programmer,
'S': Salesperson,
}
@staticmethod
def create(emp_type: str, *args, **kwargs) -> Employee:
"""Create an employee by type code.
Args:
emp_type: 'M' for Manager, 'P' for Programmer, 'S' for Salesperson.
*args, **kwargs: Passed to the employee constructor.
Raises:
ValueError: If emp_type is not recognized.
>>> emp = EmployeeFactory.create('P', 'Alice', hours_worked=160)
>>> emp.get_salary()
32000.0
"""
cls = EmployeeFactory._registry.get(emp_type.upper())
if cls is None:
valid = ', '.join(EmployeeFactory._registry.keys())
raise ValueError(f"Unknown type '{emp_type}'. Valid: {valid}")
return cls(*args, **kwargs)
@classmethod
def register(cls, type_code: str, employee_class: type):
"""Register a new employee type dynamically.
This makes the factory extensible without modifying its source.
"""
if not issubclass(employee_class, Employee):
raise TypeError(f"{employee_class} must be a subclass of Employee")
cls._registry[type_code.upper()] = employee_class
# =============================================================================
# Example 4: Polymorphic Payroll Processing
# =============================================================================
def process_payroll(employees: list[Employee]) -> None:
"""Calculate and display payroll for all employees.
Thanks to polymorphism, we don't need to check employee types.
Each employee knows how to calculate its own salary.
"""
print("=== Monthly Payroll ===")
print(f"{'Name':<15} {'Type':<15} {'Salary':>10}")
print("-" * 42)
total = 0.0
for emp in employees:
salary = emp.get_salary()
total += salary
emp_type = emp.__class__.__name__
print(f"{emp.name:<15} {emp_type:<15} ${salary:>9,.2f}")
print("-" * 42)
print(f"{'Total':<30} ${total:>9,.2f}")
print()
# =============================================================================
# Example 5: Extending the Factory
# =============================================================================
class Intern(Employee):
"""Intern with stipend (extending the system)."""
def __init__(self, name: str, stipend: float = 500.0):
super().__init__(name)
self.stipend = stipend
def get_salary(self) -> float:
return self.stipend
def demo_extensibility():
"""Show how to add new employee types without modifying existing code."""
print("=== Extending with New Types ===")
# Register new type at runtime
EmployeeFactory.register('I', Intern)
intern = EmployeeFactory.create('I', 'New Intern', stipend=800)
print(f"{intern.name}: ${intern.get_salary():.2f}")
# Demonstrate that abstract class can't be instantiated
print("\nTrying to instantiate abstract Employee...")
try:
emp = Employee("Nobody")
except TypeError as e:
print(f" TypeError: {e}")
# =============================================================================
# Main
# =============================================================================
if __name__ == '__main__':
# Create employees using factory
employees = [
EmployeeFactory.create('M', 'Alice'),
EmployeeFactory.create('P', 'Bob', hours_worked=120),
EmployeeFactory.create('P', 'Charlie', hours_worked=85),
EmployeeFactory.create('S', 'Diana', sales=123000),
]
process_payroll(employees)
demo_extensibility()