Python Decorators Demystified: From Basics to Advanced

Python Decorators Demystified: From Basics to Advanced
Decorators are one of Python’s most elegant features. They allow you to modify or enhance functions and classes without changing their source code.
Understanding the Basics
A decorator is simply a function that takes another function and extends its behavior without explicitly modifying it.
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# Output:
# Before function call
# Hello!
# After function callDecorators with Arguments
To handle functions with arguments, use *args and **kwargs:
def timer_decorator(func):
import time
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def calculate_sum(n):
return sum(range(n))
result = calculate_sum(1000000)
# Output: calculate_sum took 0.0234 seconds
print(f"Result: {result}")Preserving Function Metadata
Use functools.wraps to preserve the original function’s metadata:
from functools import wraps
def debug_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@debug_decorator
def multiply(x, y):
"""Multiply two numbers"""
return x * y
multiply(5, 3)
# Output:
# Calling multiply(5, 3)
# multiply returned 15Decorators with Parameters
Create decorators that accept arguments:
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 greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Class-Based Decorators
You can also create decorators using classes:
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} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def process_data():
print("Processing...")
process_data() # Call 1
process_data() # Call 2Practical Example: API Rate Limiting
import time
from functools import wraps
def rate_limit(max_calls, time_window):
calls = []
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove calls outside the time window
calls[:] = [call for call in calls if call > now - time_window]
if len(calls) >= max_calls:
raise Exception(f"Rate limit exceeded: {max_calls} calls per {time_window}s")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, time_window=60)
def api_call(endpoint):
print(f"Calling API: {endpoint}")
return {"status": "success"}
# Try making multiple calls
for i in range(5):
try:
api_call(f"/data/{i}")
except Exception as e:
print(f"Error: {e}")Chaining Multiple Decorators
You can stack decorators on a single function:
@timer_decorator
@debug_decorator
@repeat(times=2)
def complex_operation(x):
return x ** 2
complex_operation(5)Conclusion
Decorators are powerful tools for writing cleaner, more maintainable Python code. They’re perfect for cross-cutting concerns like logging, timing, authentication, and caching.