Today I want to talk about decorators
In Python, a decorator is a powerful and flexible design pattern that allows you to modify or extend the behavior of a function, method, or class without permanently altering its source code. Decorators leverage first-class functions and closures to wrap or "decorate" the target object, enabling reusable and clean code.
Core Concepts
-
Decorators Are Functions
A decorator is a higher-order function that:- Accepts a function/method/class as input.
- Returns a new function (or class) that enhances or replaces the original.
Example: Simple decorator to log function execution:
def logger_decorator(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}...") result = func(*args, **kwargs) # Execute the original function print(f"{func.__name__} finished.") return result return wrapper @logger_decorator def greet(name): print(f"Hello, {name}!") greet("Alice") # Output: # Calling greet... # Hello, Alice! # greet finished.
-
The
@
Syntax Sugar
The@decorator
syntax simplifies applying decorators. It is equivalent to:def greet(name): ... greet = logger_decorator(greet)
-
Preserving Metadata with
functools.wraps
Decorators replace the original function's metadata (e.g.,__name__
,__doc__
). Usefunctools.wraps
to retain them:from functools import wraps def logger_decorator(func): @wraps(func) # Preserve original metadata def wrapper(*args, **kwargs): ... return wrapper
Advanced Use Cases
-
Decorators with Arguments
To create a decorator that accepts arguments, use nested functions:def repeat(n_times): # Outer function accepts arguments def decorator(func): # Inner function accepts the target function @wraps(func) def wrapper(*args, **kwargs): for _ in range(n_times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(n_times=3) def say_hello(): print("Hello!") say_hello() # Prints "Hello!" three times
-
Class-Based Decorators
Classes can act as decorators by implementing the__call__
method:class Timer: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): import time start = time.time() result = self.func(*args, **kwargs) print(f"Time: {time.time() - start:.2f}s") return result @Timer def slow_function(): time.sleep(2) slow_function() # Output: Time: 2.00s
-
Decorating Classes
Decorators can modify classes (e.g., adding methods or properties):def add_method(cls): def new_method(self): return "This is a new method!" cls.new_method = new_method return cls @add_method class MyClass: ... obj = MyClass() print(obj.new_method()) # Output: This is a new method!
Common Use Cases
- Logging/Tracing: Track function calls and parameters.
- Timing: Measure execution time (as shown in
Timer
example). - Authorization/Validation: Check permissions or validate inputs before execution.
- Caching/Memoization: Cache results (e.g.,
functools.lru_cache
). - Retry Logic: Retry a function on failure.
- Deprecation Warnings: Mark deprecated functions.
Key Notes
- Order Matters: Multiple decorators are applied bottom-to-top:
@decorator1 @decorator2 def func(): ... # Equivalent to: func = decorator1(decorator2(func))
- Debugging: Use
__name__
or tools likeinspect
to trace decorated functions. - Performance: Avoid heavy logic in decorators if performance is critical.
Example: Caching Decorator
from functools import lru_cache
@lru_cache(maxsize=128) # Built-in decorator for caching
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(50)) # Efficiently computes with memoization