Decorators are a powerful metaprogramming tool in Python that allow you to modify or enhance functions and classes without changing their source code. They follow the decorator pattern from design patterns.
Basic Function Decorator
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
@my_decorator
def greet(name):
print(f"Hello, {name}!")
return f"Greeted {name}"
# Equivalent to: greet = my_decorator(greet)
result = greet("Alice")
Decorators with Arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(times=3)
def say_hello(name):
return f"Hello, {name}!"
# Calls the function 3 times
messages = say_hello("Bob")
Preserving Function Metadata
from functools import wraps
def timer(func):
@wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end-start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
"""This is a slow function"""
import time
time.sleep(1)
return "Done"
print(slow_function.__name__) # 'slow_function'
print(slow_function.__doc__) # 'This is a slow function'
Class Decorators
def singleton(cls):
instances = {}
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):
self.connection = "Connected"
db1 = Database()
db2 = Database()
print(db1 is db2) # True - same instance
Real-World Use Cases
# Authentication decorator
def require_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not is_authenticated():
raise PermissionError("Not authenticated")
return func(*args, **kwargs)
return wrapper
# Caching/Memoization
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Logging
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
# Rate limiting
import time
from functools import wraps
def rate_limit(max_per_second):
min_interval = 1.0 / max_per_second
def decorator(func):
last_called = [0.0]
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
Stacking Multiple Decorators
@timer
@log_calls
@require_auth
def process_payment(amount):
return f"Processed ${amount}"
# Decorators are applied from bottom to top
# Equivalent to: timer(log_calls(require_auth(process_payment)))