Decorator Basics
What is a Decorator?
A decorator is a function that takes a function and returns a new function. It's syntactic sugar for:
def my_decorator(func):
def wrapper(*args, **kwargs):
# Code before function call
result = func(*args, **kwargs)
# Code after function call
return result
return wrapper
# Using decorator
@my_decorator
def say_hello(name):
return f'Hello, {name}!'
# Equivalent to:
def say_hello(name):
return f'Hello, {name}!'
say_hello = my_decorator(say_hello)
Simple Example
def timer_decorator(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} took {end - start:.4f} seconds')
return result
return wrapper
@timer_decorator
def slow_function():
import time
time.sleep(1)
return 'Done!'
slow_function()
# slow_function took 1.0012 seconds
Preserving Function Metadata
import functools
def my_decorator(func):
@functools.wraps(func) # Preserves func's metadata
def wrapper(*args, **kwargs):
"""Wrapper documentation."""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f'Hello, {name}!'
print(greet.__name__) # greet (not 'wrapper')
print(greet.__doc__) # Greet someone.
Decorators with Parameters
Three Levels of Functions
# Level 1: Regular decorator
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# Level 2: Decorator factory (takes parameters)
def repeat(n): # Takes decorator parameters
def decorator(func): # Takes function
def wrapper(*args, **kwargs): # Takes function arguments
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# Level 3: Using the decorator
@repeat(3) # repeat(3) returns decorator
def greet(name):
print(f'Hello, {name}!')
greet('Alice')
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Common Decorator Patterns
import functools
import time
# Retry decorator
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
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!'
# Cache decorator
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) # Fast! Otherwise would be very slow
Class-Based Decorators
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f'Call {self.call_count} to {self.func.__name__}')
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print('Hello!')
say_hello() # Call 1 to say_hello
say_hello() # Call 2 to say_hello
print(say_hello.call_count) # 2
Common Decorator Patterns
Logging Decorator
import functools
import logging
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f'{k}={v!r}' for k, v in kwargs.items())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))
logging.info(f'Calling {func.__name__}({all_args})')
result = func(*args, **kwargs)
logging.info(f'{func.__name__} returned {result!r}')
return result
return wrapper
Validation Decorator
import functools
def validate_types(*types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i, (arg, type_) in enumerate(zip(args, types)):
if not isinstance(arg, type_):
raise TypeError(
f'Argument {i} must be {type_.__name__}, '
f'got {type(arg).__name__}'
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(int, int)
def add(a, b):
return a + b
add(1, 2) # ✅ 3
# add(1, '2') # ❌ TypeError
Memoization Decorator
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Or use built-in:
@functools.lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Stacking Decorators
@decorator_a
@decorator_b
@decorator_c
def my_function():
pass
# Equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))
# Order matters!
@timer
@log_calls
def my_function():
pass
# Calls log_calls first, then timer