StructuralPythonverifiedVerified
Decorator Pattern in Python
Attaches additional responsibilities to an object dynamically by wrapping it in decorator objects that share the same interface.
How to Implement the Decorator Pattern in Python
1Step 1: Define the component interface and base implementation
from typing import Protocol
class Component(Protocol):
def operation(self) -> str: ...
class ConcreteComponent:
def operation(self) -> str:
return "ConcreteComponent"2Step 2: Create the base decorator and concrete decorators
class BaseDecorator:
def __init__(self, component: Component) -> None:
self._wrappee = component
def operation(self) -> str:
return self._wrappee.operation()
class DecoratorA(BaseDecorator):
def operation(self) -> str:
return f"DecoratorA({super().operation()})"
class DecoratorB(BaseDecorator):
def operation(self) -> str:
return f"DecoratorB({super().operation()})"3Step 3: Compose decorators and observe layered behavior
def client_code(component: Component) -> None:
print("Result:", component.operation())
simple = ConcreteComponent()
client_code(simple)
# Result: ConcreteComponent
decorated = DecoratorB(DecoratorA(simple))
client_code(decorated)
# Result: DecoratorB(DecoratorA(ConcreteComponent))"""Logging and caching decorators for async services using Python decorators."""
import functools
import logging
import time
from typing import Any, Callable, Awaitable, Protocol
logger = logging.getLogger(__name__)
# [step] Define the service protocol
class UserService(Protocol):
async def get_user(self, user_id: str) -> dict[str, str]: ...
# [step] Implement the concrete service
class RealUserService:
async def get_user(self, user_id: str) -> dict[str, str]:
return {"id": user_id, "name": f"User {user_id}"}
# [step] Implement the logging decorator (class-based)
class LoggingUserService:
def __init__(self, inner: UserService) -> None:
self._inner = inner
async def get_user(self, user_id: str) -> dict[str, str]:
logger.info("[LOG] get_user called with id=%s", user_id)
start = time.monotonic()
try:
result = await self._inner.get_user(user_id)
elapsed = (time.monotonic() - start) * 1000
logger.info("[LOG] get_user succeeded in %.1fms", elapsed)
return result
except Exception:
elapsed = (time.monotonic() - start) * 1000
logger.exception("[LOG] get_user failed after %.1fms", elapsed)
raise
# [step] Implement the caching decorator (class-based)
class CachingUserService:
def __init__(self, inner: UserService, ttl_ms: int = 60_000) -> None:
self._inner = inner
self._ttl_ms = ttl_ms
self._cache: dict[str, tuple[dict[str, str], float]] = {}
async def get_user(self, user_id: str) -> dict[str, str]:
cached = self._cache.get(user_id)
if cached and time.monotonic() < cached[1]:
logger.info("[CACHE] HIT for id=%s", user_id)
return cached[0]
logger.info("[CACHE] MISS for id=%s", user_id)
value = await self._inner.get_user(user_id)
self._cache[user_id] = (value, time.monotonic() + self._ttl_ms / 1000)
return value
# [step] Compose decorators: Logging -> Caching -> Real service
service: UserService = LoggingUserService(
CachingUserService(RealUserService(), ttl_ms=30_000)
)Decorator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Decorator Pattern in the Real World
“Think of adding espresso shots and syrups to a coffee order. A plain coffee is the base component. Each addition—an espresso shot, vanilla syrup, oat milk—is a decorator that wraps the previous cup, adding its own cost and flavor. You can combine them in any order without the café needing a separate menu item for every combination.”