Decorators
Decorators¶
1. What are Decorators?¶
Decorators are a pattern that adds functionality without modifying the function or class. They use the @ syntax.
@decorator
def function():
pass
# The above code is equivalent to:
def function():
pass
function = decorator(function)
Decorator Structure¶
┌─────────────────────────────────────────┐
│ Decorator │
│ ┌─────────────────────────────────┐ │
│ │ wrapper function │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Call original func │ │ │
│ │ └─────────────────────────┘ │ │
│ │ + Additional features (before/after) │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
2. Basic Decorators¶
Simplest Form¶
def my_decorator(func):
def wrapper():
print("Before function execution")
func()
print("After function execution")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Before function execution
Hello!
After function execution
Applied to Functions with Arguments¶
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"Arguments: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
add(3, 5)
Output:
Arguments: (3, 5), {}
Result: 8
3. Preserving Metadata with @wraps¶
When decorators are applied, the original function's metadata (name, docstring, etc.) is lost.
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greeting function"""
return f"Hello, {name}"
print(greet.__name__) # wrapper (not original name!)
print(greet.__doc__) # None (docstring lost!)
Using functools.wraps¶
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserve metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greeting function"""
return f"Hello, {name}"
print(greet.__name__) # greet (preserved!)
print(greet.__doc__) # Greeting function (preserved!)
4. Decorators with Arguments¶
To pass arguments to the decorator itself, you need one more level of wrapping.
from functools import wraps
def repeat(times):
"""Decorator that repeats function execution n 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 say_hi():
print("Hi!")
say_hi()
Output:
Hi!
Hi!
Hi!
Practical Example: Permission Check¶
from functools import wraps
def require_role(role):
"""Apply to functions that require specific role"""
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
raise PermissionError(f"'{role}' permission required")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user, target_id):
print(f"User {target_id} deleted")
admin = {"name": "Alice", "role": "admin"}
guest = {"name": "Bob", "role": "guest"}
delete_user(admin, 123) # OK
# delete_user(guest, 123) # PermissionError!
5. Class-Based Decorators¶
Classes can be used as decorators by implementing the __call__ method.
class CountCalls:
"""Decorator that tracks function call count"""
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} call count: {self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # Call count: 1
say_hello() # Call count: 2
say_hello() # Call count: 3
Class Decorator with Arguments¶
class Retry:
"""Decorator that retries on failure"""
def __init__(self, max_attempts=3):
self.max_attempts = max_attempts
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == self.max_attempts:
raise
return wrapper
@Retry(max_attempts=3)
def unstable_function():
import random
if random.random() < 0.7:
raise ValueError("Random failure!")
return "Success!"
result = unstable_function()
6. Built-in Decorators¶
@property¶
Define getter/setter/deleter.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Radius (read)"""
return self._radius
@radius.setter
def radius(self, value):
"""Radius (write)"""
if value < 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def area(self):
"""Area (computed property)"""
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5
print(circle.area) # 78.53975
circle.radius = 10
print(circle.area) # 314.159
@staticmethod¶
Method that doesn't access instance or class.
class Math:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call without instance
print(Math.add(3, 5)) # 8
print(Math.multiply(3, 5)) # 15
@classmethod¶
Method that receives class as first argument.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
"""Create Date with today's date"""
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
date1 = Date.from_string("2024-01-23")
date2 = Date.today()
print(date1) # Date(2024, 1, 23)
7. Practical Decorator Patterns¶
Timing Measurement¶
import time
from functools import wraps
def timer(func):
"""Measure function execution time"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__}: {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
slow_function() # slow_function: 1.0012s
Logging¶
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
def log_calls(func):
"""Log function calls"""
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling: {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
logging.info(f"Returned: {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
Caching (Memoization)¶
from functools import wraps
def memoize(func):
"""Decorator that caches results"""
cache = {}
@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)
print(fibonacci(100)) # Very slow without caching
Note: Use Python's built-in functools.lru_cache for more convenience.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Input Validation¶
from functools import wraps
def validate_types(**expected_types):
"""Decorator that validates argument types"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Validate keyword arguments
for name, expected in expected_types.items():
if name in kwargs:
if not isinstance(kwargs[name], expected):
raise TypeError(
f"{name} must be of type {expected.__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
create_user(name="Alice", age=30) # OK
# create_user(name="Alice", age="30") # TypeError!
8. Decorator Chaining¶
Multiple decorators can be applied simultaneously. Application order is bottom to top.
@decorator1
@decorator2
@decorator3
def func():
pass
# The above code is equivalent to:
func = decorator1(decorator2(decorator3(func)))
Example¶
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("World")) # <b><i>Hello, World</i></b>
9. Class Decorators¶
Decorators can be applied to entire classes.
def singleton(cls):
"""Singleton pattern decorator"""
instances = {}
@wraps(cls)
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):
print("Creating database connection")
db1 = Database() # Creating database connection
db2 = Database() # (no output - same instance)
print(db1 is db2) # True
dataclass (Built-in Class Decorator)¶
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p) # Point(x=3, y=4)
print(p.distance()) # 5.0
print(p == Point(3, 4)) # True
10. Summary¶
| Pattern | Description | Example |
|---|---|---|
| Basic decorator | Wrap function to add functionality | @timer |
| Decorator with args | Pass configuration to decorator | @repeat(3) |
| Class-based decorator | When state needs to be maintained | @CountCalls |
| @wraps | Preserve metadata | @wraps(func) |
| @property | Define getter/setter | @property |
| @staticmethod | Static method | @staticmethod |
| @classmethod | Class method | @classmethod |
| @lru_cache | Cache results | @lru_cache(128) |
11. Practice Problems¶
Exercise 1: Execution Time Limit¶
Create a decorator that raises TimeoutError if the function doesn't complete within a specified time.
Exercise 2: Result Logging¶
Create a decorator that logs both input and output of a function to a file.
Exercise 3: Debug Mode¶
Create a decorator that outputs debug information only when a DEBUG flag is True.
Next Steps¶
Check out 03_Context_Managers.md to learn about with statements and resource management!