Декораторы
Декораторы -- один из самых элегантных паттернов Python. Они позволяют модифицировать поведение функций и классов без изменения их исходного кода.
Что такое декоратор
Декоратор -- это функция, которая принимает функцию и возвращает новую (модифицированную) функцию:
# Decorator is just syntactic sugar
def my_decorator(func):
def wrapper(*args, **kwargs):
print("До вызова")
result = func(*args, **kwargs)
print("После вызова")
return result
return wrapper
# These two are equivalent:
@my_decorator
def greet(name: str) -> str:
return f"Привет, {name}!"
# Same as: greet = my_decorator(greet)
print(greet("Иван"))
# До вызова
# После вызова
# Привет, Иван!
@functools.wraps -- обязательный элемент
Без @wraps декоратор теряет метаданные исходной функции:
import functools
def bad_decorator(func):
"""Decorator WITHOUT @wraps."""
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def hello():
"""Say hello."""
pass
print(hello.__name__) # 'wrapper' (WRONG! Should be 'hello')
print(hello.__doc__) # None (WRONG! Should be 'Say hello.')
# CORRECT: always use @functools.wraps
def good_decorator(func):
"""Decorator WITH @wraps."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def hello():
"""Say hello."""
pass
print(hello.__name__) # 'hello'
print(hello.__doc__) # 'Say hello.'
print(hello.__wrapped__) # original function (access through __wrapped__)
Практические декораторы
Замер времени выполнения
import functools
import time
def timer(func):
"""Measure and print execution time."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed:.4f}с")
return result
return wrapper
@timer
def slow_function():
"""Simulate slow work."""
time.sleep(0.5)
return "done"
slow_function() # slow_function: 0.5003с
Логирование вызовов
import functools
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def log_calls(func):
"""Log function calls with arguments and return value."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.debug(f"Вызов {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
logger.debug(f"{func.__name__} вернул {result!r}")
return result
except Exception as e:
logger.exception(f"{func.__name__} вызвал исключение: {e}")
raise
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
add(2, 3)
# DEBUG: Вызов add(2, 3)
# DEBUG: add вернул 5
Retry с экспоненциальной задержкой
import functools
import time
from typing import TypeVar, Callable, Any
def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
"""Retry decorator with exponential backoff."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt < max_attempts:
print(f"Попытка {attempt} неудачна: {e}. "
f"Повтор через {current_delay:.1f}с...")
time.sleep(current_delay)
current_delay *= backoff
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, backoff=2.0)
def unreliable_api_call() -> str:
"""Simulate an unreliable API."""
import random
if random.random() < 0.7:
raise ConnectionError("Сервер недоступен")
return "OK"
Кэширование (Memoization)
import functools
def cache(func):
"""Simple memoization decorator."""
memo = {}
@functools.wraps(func)
def wrapper(*args):
if args not in memo:
memo[args] = func(*args)
return memo[args]
wrapper.cache = memo # expose cache for debugging
wrapper.cache_clear = memo.clear
return wrapper
@cache
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 354224848179261915075
# Better: use built-in functools.lru_cache
@functools.lru_cache(maxsize=128)
def factorial(n: int) -> int:
return 1 if n <= 1 else n * factorial(n - 1)
print(factorial(20)) # 2432902008176640000
print(factorial.cache_info())
# CacheInfo(hits=0, misses=21, maxsize=128, currsize=21)
# Python 3.9+: functools.cache (unlimited cache)
@functools.cache
def expensive_computation(x: int) -> int:
"""Unlimited cache (no maxsize)."""
print(f"Computing for {x}...")
return x ** 3
print(expensive_computation(5)) # Computing for 5... 125
print(expensive_computation(5)) # 125 (cached, no print)
Валидация аргументов
import functools
def validate_types(**type_hints):
"""Validate function argument types at runtime."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Get function parameter names
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in type_hints.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"Аргумент '{param_name}' должен быть {expected_type.__name__}, "
f"получен {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name: str, age: int) -> dict:
return {"name": name, "age": age}
print(create_user("Иван", 25)) # {'name': 'Иван', 'age': 25}
# create_user("Иван", "25") # TypeError: Аргумент 'age' должен быть int
Стекирование декораторов
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"Время: {time.perf_counter() - start:.4f}с")
return result
return wrapper
def log_result(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"Результат: {result}")
return result
return wrapper
# Decorators apply from bottom to top
@timer # 2nd: wraps the result of @log_result
@log_result # 1st: wraps the original function
def compute(n: int) -> int:
return sum(range(n))
compute(1_000_000)
# Результат: 499999500000
# Время: 0.0312с
# Equivalent to: compute = timer(log_result(compute))
Декораторы-классы
import functools
class CountCalls:
"""Decorator that counts function calls."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} вызвана {self.count} раз")
return self.func(*args, **kwargs)
def reset(self):
self.count = 0
@CountCalls
def greet(name: str) -> str:
return f"Привет, {name}!"
greet("Иван") # greet вызвана 1 раз
greet("Мария") # greet вызвана 2 раз
print(f"Всего вызовов: {greet.count}") # 2
greet.reset()
# Class decorator with arguments
class RateLimit:
"""Limit function calls per time window."""
def __init__(self, max_calls: int, period: float):
self.max_calls = max_calls
self.period = period
self.calls = []
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import time
now = time.time()
# Remove old calls outside the window
self.calls = [t for t in self.calls if now - t < self.period]
if len(self.calls) >= self.max_calls:
raise RuntimeError(f"Rate limit: max {self.max_calls} calls per {self.period}s")
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimit(max_calls=3, period=10.0)
def api_call(endpoint: str) -> str:
return f"Response from {endpoint}"
Встроенные декораторы
# @staticmethod - no access to instance or class
# @classmethod - receives class as first argument
# @property - getter/setter as attribute access
# @functools.lru_cache - memoization
# @functools.cache - unlimited memoization (Python 3.9+)
# @functools.singledispatch - function overloading by type
# @dataclasses.dataclass - auto-generate __init__, __repr__, etc.
# @abc.abstractmethod - abstract methods in ABC
# @functools.singledispatch example
from functools import singledispatch
@singledispatch
def format_value(value) -> str:
"""Format a value to string."""
return str(value)
@format_value.register(int)
def _(value: int) -> str:
return f"{value:,}"
@format_value.register(float)
def _(value: float) -> str:
return f"{value:.2f}"
@format_value.register(list)
def _(value: list) -> str:
return f"[{', '.join(str(x) for x in value)}]"
print(format_value(1000000)) # 1,000,000
print(format_value(3.14159)) # 3.14
print(format_value([1, 2, 3])) # [1, 2, 3]
print(format_value("hello")) # hello
Итоги
- Декоратор -- функция, принимающая функцию и возвращающая новую функцию
@functools.wraps-- обязательно для сохранения метаданных- Декоратор с аргументами -- три уровня вложенности
- Стекирование:
@A @B def f()=f = A(B(f))(снизу вверх) @functools.lru_cache/@functools.cache-- встроенное кэширование@functools.singledispatch-- перегрузка по типу первого аргумента- Классы-декораторы -- когда нужно хранить состояние
- Практические применения: timing, logging, retry, validation, rate limiting