Let’s continue with more design patterns. Another brilliant design pattern comes from Python's contextlib and the way it implements context managers. The genius part is how it uses generators to create context managers, making complex resource management look almost trivial. Here's a simplified version showing the core idea:
from contextlib import contextmanager
import time
class Timer:
def __init__(self):
self.elapsed = 0
@contextmanager
def measure(self, operation_name):
start = time.time()
try:
yield # This is where the user's code will run
finally:
self.elapsed = time.time() - start
print(f"{operation_name} took {self.elapsed:.2f} seconds")
# Usage
timer = Timer()
with timer.measure("data processing"):
# Simulate some work
time.sleep(1)
What's brilliant about this design is how it leverages Python's generator syntax to create a clean, readable way to handle setup and teardown code. The yield
statement creates a natural separation between setup and cleanup code, and the contextmanager
decorator handles all the complexity of implementing the context manager protocol.
We can adapt this pattern for many other use cases where we need to manage resources or track state changes. Here's an example of how we might use it for transaction management in a custom data structure:
class VersionedDict:
def __init__(self):
self._data = {}
self._history = []
@contextmanager
def transaction(self):
snapshot = self._data.copy()
try:
yield self
self._history.append(snapshot) # Commit
except Exception:
self._data = snapshot # Rollback
raise
def set(self, key, value):
self._data[key] = value
def get(self, key):
return self._data.get(key)
def undo(self):
if self._history:
self._data = self._history.pop()
# Usage
d = VersionedDict()
with d.transaction():
d.set('a', 1)
d.set('b', 2)
# If any exception occurs, changes are rolled back
This pattern showcase how Python's features can be combined in clever ways to create elegant, powerful abstractions. This can be adapted for many different use cases where you need to build complex operations incrementally or manage resources safely. Understanding these patterns helps write more maintainable and elegant code.
One practical application is in configuration management systems, where you might want to track changes to settings and roll them back if they cause problems:
class ConfigurationManager:
def __init__(self):
self._settings = {}
self._snapshots = []
self._active_snapshot = None
self._validation_hooks = []
def add_validation(self, validator):
"""Add a validation function that must pass for changes to be committed"""
self._validation_hooks.append(validator)
@contextmanager
def batch_update(self, description):
# Create a snapshot of current settings
previous_settings = self._settings.copy()
self._active_snapshot = {
'description': description,
'timestamp': time.time(),
'settings': previous_settings
}
try:
yield self
# Validate all changes
for validator in self._validation_hooks:
if not validator(self._settings):
raise ValueError(f"Validation failed for {description}")
# If we get here, all validations passed
self._snapshots.append(self._active_snapshot)
except Exception as e:
# Restore previous settings on any error
self._settings = previous_settings
raise
finally:
self._active_snapshot = None
def set(self, key, value):
self._settings[key] = value
def get(self, key, default=None):
return self._settings.get(key, default)
def rollback_to(self, timestamp):
"""Roll back to the state at or before the given timestamp"""
for snapshot in reversed(self._snapshots):
if snapshot['timestamp'] <= timestamp:
self._settings = snapshot['settings'].copy()
return True
return False
# Example usage in a web server configuration system
def validate_port(settings):
port = settings.get('port', 80)
return 0 <= port <= 65535
def validate_host(settings):
host = settings.get('host', 'localhost')
return bool(host.strip())
config = ConfigurationManager()
config.add_validation(validate_port)
config.add_validation(validate_host)
# Update multiple related settings atomically
try:
with config.batch_update("Update server settings"):
config.set('port', 8080)
config.set('host', 'example.com')
config.set('max_connections', 1000)
except ValueError:
print("Configuration update failed validation")
# Roll back to a previous state if needed
config.rollback_to(time.time() - 3600) # Roll back to 1 hour ago
The power of this pattern lies in how it combines several important software design principles:
Atomicity: Changes are all-or-nothing, preventing partial updates that could leave the system in an inconsistent state.
Isolation: Changes within a transaction are isolated from other parts of the system until they're committed.
Durability: The history of changes is preserved, allowing for recovery from errors.
Validation: The pattern can easily incorporate validation rules that must pass before changes are committed.
Hope you enjoyed!