9 min read
ā¢Question 33 of 41hardAdvanced Decorator Patterns
Complex decorator implementations.
What You'll Learn
- Class-based decorators for stateful behavior
- Decorators with optional arguments
- Stacking and order of decorators
- Decorating methods and classes
- The importance of functools.wraps
Why functools.wraps Matters
Without @wraps, decorated functions lose their identity:
code.pyPython
from functools import wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def good_decorator(func):
@wraps(func) # Preserves function metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def bad_example():
"""This is a docstring."""
pass
@good_decorator
def good_example():
"""This is a docstring."""
pass
print(bad_example.__name__) # "wrapper" - WRONG
print(bad_example.__doc__) # None - WRONG
print(good_example.__name__) # "good_example" - CORRECT
print(good_example.__doc__) # "This is a docstring." - CORRECTClass-Based Decorators
Use classes when you need to maintain state:
code.pyPython
from functools import update_wrapper
class CountCalls:
def __init__(self, func):
update_wrapper(self, func) # Like @wraps for classes
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # Call #1 to say_hello
say_hello() # Call #2 to say_hello
print(say_hello.count) # 2
# Reset if needed
say_hello.count = 0Decorators with Optional Arguments
Create decorators that work with or without parentheses:
code.pyPython
from functools import wraps
def repeat(_func=None, *, times=2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
# Called without arguments: @repeat
if _func is not None:
return decorator(_func)
# Called with arguments: @repeat(times=3)
return decorator
@repeat # Works! (times defaults to 2)
def say_hi():
print("Hi")
@repeat(times=3) # Also works!
def say_hello():
print("Hello")
say_hi() # Prints "Hi" twice
say_hello() # Prints "Hello" three timesStacking Decorators
Decorators apply bottom-up, execute top-down:
code.pyPython
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
def underline(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<u>{func(*args, **kwargs)}</u>"
return wrapper
@bold # Applied last, executes first
@italic # Applied second
@underline # Applied first, executes last
def greet(name):
return f"Hello, {name}"
print(greet("World"))
# <b><i><u>Hello, World</u></i></b>
# Equivalent to:
# greet = bold(italic(underline(greet)))Decorating Methods
Handle self properly for instance methods:
code.pyPython
from functools import wraps
import time
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs): # Works for functions and methods
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def log_method(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
print(f"{self.__class__.__name__}.{func.__name__}({args}, {kwargs})")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
@timing
@log_method
def add(self, a, b):
return a + b
calc = Calculator()
calc.add(2, 3)
# Calculator.add((2, 3), {})
# add took 0.0001sMemoization with Decorator
code.pyPython
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
# Expose cache for inspection and clearing
wrapper.cache = cache
wrapper.cache_clear = lambda: cache.clear()
wrapper.cache_info = lambda: f"Size: {len(cache)}"
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Instant!
print(fibonacci.cache_info()) # Size: 101
# Note: Use functools.lru_cache in production
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)Class Decorators
Decorators can modify or wrap entire classes:
code.pyPython
def singleton(cls):
"""Make a class a singleton."""
instances = {}
@wraps(cls, updated=[])
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("Initializing database")
db1 = Database() # Initializing database
db2 = Database() # (nothing - returns cached)
print(db1 is db2) # True
# Add methods to class
def add_repr(cls):
def __repr__(self):
attrs = ', '.join(f'{k}={v!r}' for k, v in vars(self).items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
print(Person("Alice", 30)) # Person(name='Alice', age=30)Decorators with Arguments (Full Pattern)
code.pyPython
from functools import wraps
def retry(max_attempts=3, exceptions=(Exception,)):
"""Retry decorator with configurable attempts."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
print(f"Attempt {attempt} failed: {e}")
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ConnectionError, TimeoutError))
def fetch_data():
# Might fail sometimes
import random
if random.random() < 0.7:
raise ConnectionError("Network error")
return "Data"Interview Tip
When asked about advanced decorators:
- Always use @wraps to preserve function metadata
- Use class-based decorators for stateful behavior
- Decorators with optional args: check if _func is None
- Stack order: bottom decorator applies first
- For memoization, prefer functools.lru_cache