def Decorator / @ Syntax
A decorator is a mechanism that takes a function or class and returns a new function (or class) with added functionality. Writing @decorator_name immediately before a function definition applies the decorator at the time the function is defined. Always use functools.wraps() when writing your own decorators to preserve the original function's metadata.
Syntax
from functools import wraps
# Define a decorator
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Pre-processing
result = func(*args, **kwargs)
# Post-processing
return result
return wrapper
# Apply the decorator
@my_decorator
def my_function():
pass
# Decorator that accepts arguments
def decorator_with_args(arg1, arg2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
Syntax / Functions
| Syntax / Function | Description |
|---|---|
| @decorator_name | Written before a function definition to apply the decorator. |
| @wraps(func) | Copies the metadata of the original func (such as __name__) to the wrapper. |
| *args, **kwargs | A pattern used inside a decorator to accept all arguments of the wrapped function. |
| @deco1 @deco2 | Stacking decorators. They are applied from bottom to top (deco2 first, then deco1). |
Sample Code
from functools import wraps
import time
# Decorator that measures execution time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[{func.__name__}] elapsed: {elapsed:.4f}s")
return result
return wrapper
@timer
def heavy_process(n):
"""Simulates a heavy computation"""
return sum(range(n))
print(heavy_process(1000000)) # [heavy_process] elapsed: 0.XXXXs
# Decorator with arguments (retry on failure)
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {i+1}/{times} failed: {e}")
raise RuntimeError(f"{func.__name__} failed after {times} attempts")
return wrapper
return decorator
@retry(times=3)
def unstable_api():
import random
if random.random() < 0.7:
raise ConnectionError("Connection error")
return "Success"
# Stacking decorators (applying multiple decorators)
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"**{func(*args, **kwargs)}**"
return wrapper
def upper(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@bold # Applied second (outer)
@upper # Applied first (inner)
def greet(name):
return f"hello, {name}"
print(greet('world')) # **HELLO, WORLD** (upper → bold order)
# Class-based decorator
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
wraps(func)(self) # Copy metadata
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
return self.cache[args]
@Memoize
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30)) # 832040 (fast thanks to caching)
Notes
A decorator is syntactic sugar in Python. Writing @my_decorator is equivalent to func = my_decorator(func). When decorators are stacked, they are applied from the bottom up (innermost to outermost).
A decorator that accepts arguments requires three levels of nested functions, but the structure is straightforward once you understand it: the outer function receives the arguments, the middle function receives the target function, and the innermost wrapper performs the actual wrapping logic.
Decorators are ideal for implementing cross-cutting concerns such as logging, authentication checks, caching, and validation. They help avoid code duplication and support the DRY (Don't Repeat Yourself) principle.
If you find any errors or copyright issues, please contact us.