Python Decorators
Decorators are a powerful feature that allows you to modify the behavior of functions or classes without changing their source code. They are commonly used for logging, authentication, caching, and more.
What are Decorators?
Decorators are functions that take another function and extend its behavior without modifying it.
Basic Decorator
def my_decorator(func):
def wrapper():
print("Something before the function.")
func()
print("Something after the function.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# Using the decorator
say_hello()The @ syntax applies the decorator. my_decorator returns wrapper, which calls the original function. See function definitions for basics.
Manual Decoration
def my_decorator(func):
def wrapper():
print("Before")
func()
print("After")
return wrapper
def say_hello():
print("Hello!")
# Manual decoration (equivalent to @ syntax)
decorated_function = my_decorator(say_hello)
decorated_function()The @ syntax is just syntactic sugar for function wrapping.
Decorators with Arguments
Decorators can accept arguments for configuration.
Decorator with Parameters
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")repeat(3) returns a decorator, which is applied to greet. See closures for how this works.
Function Arguments
Decorators must handle any function signature.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
@logger
def greet(name, prefix="Hello"):
return f"{prefix}, {name}!"
add(5, 3)
greet("Bob", prefix="Hi")*args and **kwargs capture all arguments. Essential for flexible decorators.
Built-in Decorators
Python provides useful built-in decorators.
@staticmethod
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call without creating instance
result = MathUtils.add(5, 3)
print(result) # 8Static methods don’t need self and can be called on the class. See static methods.
@classmethod
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
age = 2023 - birth_year
return cls(name, age)
person = Person.from_birth_year("Alice", 1990)
print(f"{person.name} is {person.age} years old")Class methods receive the class as first argument. Useful for alternative constructors.
@property
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
@property
def area(self):
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(f"Area: {circle.area}") # No parentheses needed
circle.radius = 10
print(f"New area: {circle.area}")Properties allow method calls to look like attribute access. See property.
Practical Decorators
Timing Decorator
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {(end - start) * 1000:.2f} ms")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done!"
@timer
def fast_function():
return sum(range(1000000))
slow_function()
fast_function()Measure function execution time. Useful for performance monitoring.
Caching Decorator
def cache(func):
cache_dict = {}
def wrapper(*args, **kwargs):
# Create hashable key from arguments
key = (args, tuple(sorted(kwargs.items())))
if key not in cache_dict:
cache_dict[key] = func(*args, **kwargs)
return cache_dict[key]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # Calculates once
print(fibonacci(10)) # Returns cached resultCache expensive function calls. Prevents redundant computations.
Authentication Decorator
def requires_auth(func):
def wrapper(user, *args, **kwargs):
if not hasattr(user, 'is_authenticated') or not user.is_authenticated:
raise PermissionError("User must be authenticated")
return func(user, *args, **kwargs)
return wrapper
class User:
def __init__(self, name, authenticated=False):
self.name = name
self.is_authenticated = authenticated
@requires_auth
def view_profile(user):
return f"Profile of {user.name}"
user1 = User("Alice", authenticated=True)
user2 = User("Bob", authenticated=False)
print(view_profile(user1))
# print(view_profile(user2)) # Raises PermissionErrorCheck authentication before allowing function execution.
functools.wraps
Preserve original function metadata when decorating.
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves func metadata
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
print("Calling decorated function")
return func(*args, **kwargs)
return wrapper
@my_decorator
def example():
"""Original function docstring"""
return "Hello"
print(example.__name__) # "example" (not "wrapper")
print(example.__doc__) # "Original function docstring"wraps copies __name__, __doc__, etc. Essential for debugging and documentation.
Class Decorators
Decorators can also be applied to classes.
Class Decorator
def singleton(cls):
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 DatabaseConnection:
def __init__(self, host):
self.host = host
print(f"Connecting to {host}")
# Only one instance created
db1 = DatabaseConnection("localhost")
db2 = DatabaseConnection("localhost")
print(db1 is db2) # TrueEnsure only one instance of a class exists. Singleton pattern.
Dataclass Decorator
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
city: str = "Unknown"
person = Person("Alice", 30)
print(person) # Person(name='Alice', age=30, city='Unknown')
print(person.name) # Alicedataclass automatically generates __init__, __repr__, etc. See dataclasses.
Decorator Stacking
Apply multiple decorators to one function.
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("Alice")) # <b><i>Hello, Alice!</i></b>Decorators apply from bottom to top. @bold wraps @italic which wraps greet.
Common Patterns
Retry Decorator
def retry(max_attempts=3, delay=1):
def decorator(func):
@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 e
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
def unreliable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("Network error")
return "Success!"
result = unreliable_api_call()
print(result)Retry failed operations automatically.
Deprecation Decorator
import warnings
def deprecated(func):
@wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(f"{func.__name__} is deprecated",
DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
return wrapper
@deprecated
def old_function():
return "This function is old"
old_function() # Issues deprecation warningWarn users about deprecated functions.
Best Practices
- Use
functools.wrapsto preserve function metadata - Keep decorators simple and focused on one concern
- Use
*args, **kwargsfor flexible argument handling - Consider performance impact of decorators
- Document decorator behavior and requirements
- Test decorated functions thoroughly
External Resources:
Related Tutorials: