Python Decorators
Decorators are one of Python's most powerful features. They wrap a function to add behaviour without modifying its source code. Understanding them is essential for Python interviews and for writing clean, reusable code.
Functions Are First-Class Objects
def greet():
return "Hello!"
# Functions can be assigned to variables
say_hi = greet
print(say_hi()) # Hello!
# Functions can be passed as arguments
def run(func):
return func()
print(run(greet)) # Hello!
# Functions can be returned from other functions
def make_greeter(name):
def inner():
return f"Hello, {name}!"
return inner # return function, not its result
hello_rahul = make_greeter("Rahul")
print(hello_rahul()) # Hello, Rahul!
Your First Decorator
import time
from functools import wraps
def timer(func):
@wraps(func) # preserves original function metadata
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # call original
end = time.perf_counter()
print(f"{func.__name__} took {end-start:.4f}s")
return result
return wrapper
@timer # equivalent to: slow_function = timer(slow_function)
def slow_function():
time.sleep(0.1)
return "done"
slow_function() # slow_function took 0.1001s
Decorator with Arguments
def retry(max_attempts=3, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url):
# simulated network call
raise ConnectionError("Network down")
# fetch_data("https://api.example.com")
# Attempt 1 failed: Network down
# Attempt 2 failed: Network down
# Attempt 3 failed: Network down
# raises ConnectionError
Practical Decorators
from functools import wraps
# 1. Login Required
def login_required(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.get('user'):
return {'error': 'Login required', 'code': 401}
return func(request, *args, **kwargs)
return wrapper
# 2. Cache / Memoize
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
print(fib(50)) # instant
# 3. Rate Limiter
import time
from collections import defaultdict
def rate_limit(calls_per_second=1):
last_called = defaultdict(float)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[func.__name__]
if elapsed < 1.0 / calls_per_second:
time.sleep(1.0 / calls_per_second - elapsed)
last_called[func.__name__] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
# 4. Type Validation
def validate_types(**type_map):
def decorator(func):
@wraps(func)
def wrapper(**kwargs):
for param, expected in type_map.items():
if param in kwargs and not isinstance(kwargs[param], expected):
raise TypeError(f"{param} must be {expected.__name__}")
return func(**kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {'name': name, 'age': age}
create_user(name='Rahul', age=25) # OK
create_user(name='Rahul', age='25') # TypeError: age must be int
Class-Based Decorators
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # copy metadata
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def process():
return "processed"
process() # process called 1 times
process() # process called 2 times
print(process.count) # 3
Q: Why use @wraps in decorators?
Without @wraps, the wrapped function loses its __name__, __doc__, and other metadata. If you call help() or inspect the function, you see the wrapper's metadata instead. @wraps(func) copies the original function's attributes to the wrapper.
Q: What is the execution order of stacked decorators?
Decorators apply bottom-up at definition time but execute top-down at call time. @a @b def f(): pass is equivalent to f = a(b(f)). When called, a's wrapper runs first, then b's wrapper, then the original function.
Comments (0)
No comments yet. Be the first!
Leave a Comment