Decorators are one of Python's more "magic-looking" features — but they're really just functions that take a function and return a new one.
Starting from First Principles
Since functions are objects, you can pass them around and wrap them:
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return "hello"
greet = shout(greet)
print(greet()) # "HELLO"
The @ syntax is just sugar for exactly this pattern:
@shout
def greet():
return "hello"
print(greet()) # "HELLO"
@shout above def greet is equivalent to writing greet = shout(greet).
Handling Arguments
Real functions take arguments, so decorators typically use *args and **kwargs to forward whatever was passed in:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(1)
return a + b
slow_add(2, 3)
# slow_add took 1.0001s
Preserving Metadata with functools.wraps
Without care, a decorated function loses its original name and docstring:
print(slow_add.__name__) # "wrapper" — not helpful!
functools.wraps fixes this:
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
Now slow_add.__name__ correctly reports "slow_add".
Decorators with Arguments
Sometimes you want to configure the decorator itself, which requires an extra layer of nesting:
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi!")
say_hi()
# Hi!
# Hi!
# Hi!
Common Real-World Uses
- Logging: record every call to a function
- Caching:
functools.lru_cachememoizes results automatically - Access control: check permissions before running a view function (common in Flask/Django)
- Retry logic: automatically retry a function on failure
- Validation: check argument types or values before execution
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Class-Based Decorators
Decorators don't have to be functions — any callable works, including classes with __call__:
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
Key Takeaway
Decorators are just functions wrapping functions — a clean way to add behavior (timing, caching, logging, validation) without cluttering the core logic of the function itself. Once you see through the @ syntax to the underlying func = decorator(func) pattern, they stop feeling like magic.
Comments (0)
No comments yet
Be the first to share your thoughts!