Python Decorators
Introduction
Decorators are a powerful feature in Python that allows you to modify the behavior of functions and classes. This guide covers various types of decorators and their applications.
Basic Decorators
Function Decorators
| def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
latency = time.time() - start
return {"latency": latency, "result": result}
return wrapper
@timing
def my_function():
pass
|
Class Decorators
| def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class MyClass:
pass
|
Advanced Decorator Patterns
Decorator with Arguments
| 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(times=3)
def greet(name):
print(f"Hello, {name}!")
|
Method Decorators
| class inject_db_driver:
def __init__(self, function):
self.function = function
wraps(self.function)(self)
def __call__(self, dbstring):
return self.function(lambda dbstring: dbstring)
def __get__(self, instance, owner):
if instance is None:
return self
return self.__class__(MethodType(self.function, instance))
class DataHandler:
@inject_db_driver
def run_query(self, driver):
return "test"
|
Coroutine Decorators
| def timing(callable):
@wraps(callable)
def wrapped(*args, **kwargs):
start = time.time()
result = callable(*args, **kwargs)
latency = time.time() - start
return {"latency": latency, "result": result}
@wraps(callable)
async def wrapped_coro(*args, **kwargs):
start = time.time()
result = await callable(*args, **kwargs)
latency = time.time() - start
return {"latency": latency, "result": result}
if inspect.iscoroutinefunction(callable):
return wrapped_coro
return wrapped
|
Decorator Composition
Multiple Decorators
| @decorator1
@decorator2
@decorator3
def function():
pass
|
Decorator Factory
| def _log(f, *args, **kwargs):
print(f"calling {f.__qualname__!r} with {args=} and {kwargs=}")
return f(*args, **kwargs)
@(lambda f: lambda *args, **kwargs: _log(f, *args, **kwargs))
def func(x):
return x + 1
|
Practical Examples
Serialization Decorator
| @dataclass
class Serializer:
def __init__(self, dict_values):
self.values = dict_values
def serialize(self, object):
return [trans(getattr(object, field))
for field, trans in self.values.items()]
def serialize(**trans):
serializer = Serializer(trans)
def wrapper(class_obj):
def inner(instance):
return serializer.serialize(instance)
class_obj.serialize = inner
return class_obj
return wrapper
@serialize(username=str, password=str, ip=str)
@dataclass
class Event:
username: int
password: int
ip: int
|
Resolver Mixin
| def with_resolver(cls):
def _resolver_method(self, attr):
if attr.startswith("resolve_"):
*_, actual_attr = attr.partition("resolve_")
else:
actual_attr = attr
try:
return self.__dict__[actual_attr]
except KeyError as e:
raise AttributeError from e
cls.__getattr__ = _resolver_method
return cls
@dataclass
@with_resolver
class Customer:
customer_id: str
name: str
address: str
|
Best Practices
- Always use
@wraps
to preserve function metadata
- Keep decorators simple and focused
- Use decorator factories for configurable decorators
- Consider using classes for complex decorators
- Document decorator behavior and requirements
- Handle both synchronous and asynchronous functions
- Use type hints for better code clarity