Decorators are functions that modify or enhance other functions without changing their code directly.
# Basic decorator
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 call
# Equivalent to:
# say_hello = my_decorator(say_hello)
# Decorator that handles function arguments
def timer(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
def slow_function(n):
import time
time.sleep(n)
return f"Slept for {n} seconds"
result = slow_function(1)
print(result)
# Preserving function metadata
from functools import wraps
def better_timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
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
# Decorator that takes parameters
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(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
# Retry decorator
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if random.random() < 0.7:
raise Exception("Random failure")
return "Success!"
# Decorator as a class
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} has been called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # say_hello has been called 1 times
say_hello() # say_hello has been called 2 times
# Parameterized class decorator
class RateLimit:
def __init__(self, max_calls=10, time_window=60):
self.max_calls = max_calls
self.time_window = time_window
self.calls = []
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
now = time.time()
# Remove old calls outside time window
self.calls = [call_time for call_time in self.calls
if now - call_time < self.time_window]
if len(self.calls) >= self.max_calls:
raise Exception(f"Rate limit exceeded: {self.max_calls} calls per {self.time_window}s")
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimit(max_calls=3, time_window=10)
def api_call():
return "API response"
# Authentication decorator
def requires_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
# In a real app, check session, token, etc.
if not hasattr(wrapper, 'authenticated'):
raise PermissionError("Authentication required")
return func(*args, **kwargs)
return wrapper
# Cache decorator
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(sorted(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Validation decorator
def validate_types(**expected_types):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Get function signature
import inspect
sig = inspect.signature(func)
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
for param_name, expected_type in expected_types.items():
if param_name in bound_args.arguments:
value = bound_args.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(f"{param_name} must be {expected_type.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return f"User: {name}, Age: {age}"
# Using @property, @setter, @deleter
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@radius.deleter
def radius(self):
print("Deleting radius")
self._radius = 0
@property
def area(self):
return 3.14159 * self._radius ** 2
@property
def diameter(self):
return 2 * self._radius
circle = Circle(5)
print(circle.radius) # 5
print(circle.area) # 78.53975
circle.radius = 10 # Uses setter
print(circle.diameter) # 20
del circle.radius # Uses deleter
# Multiple decorators on one function
@timer
@retry(max_attempts=2)
@memoize
def expensive_calculation(n):
import time
time.sleep(0.1) # Simulate expensive operation
return n ** 2
# Equivalent to:
# expensive_calculation = timer(retry(max_attempts=2)(memoize(expensive_calculation)))
result = expensive_calculation(5)
print(result)
# Order matters!
@repeat(2)
@timer
def test_function():
print("Testing...")
# This will time each repetition separately
@timer
@repeat(2)
def test_function2():
print("Testing...")
# This will time the entire repeated execution
@wraps(func)
to preserve function metadata*args
and **kwargs
to support any function signature