PythonJuniorCoding
Что такое декораторы в Python и как их реализовать?
Декоратор — функция, принимающая функцию и возвращающая новую функцию; синтаксис @decorator эквивалентен fn = decorator(fn); используйте @functools.wraps для сохранения метаданных оригинальной функции.
Что такое декоратор
Декоратор — это вызываемый объект, который принимает функцию (или класс) и возвращает другую функцию (или класс). Синтаксис @my_decorator над определением функции — синтаксический сахар для fn = my_decorator(fn). Декораторы используются для добавления поведения без изменения исходного кода: логирование, кеширование, авторизация, retry, валидация.
Базовый декоратор
import functools
import time
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
def timer(fn: F) -> F:
@functools.wraps(fn) # копирует __name__, __doc__, __annotations__
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.3f}s")
return result
return wrapper # type: ignore[return-value]
@timer
def fetch_jobs(limit: int = 100) -> list[str]:
time.sleep(0.05)
return [f"Job {i}" for i in range(limit)]
fetch_jobs(10)
# fetch_jobs took 0.050s
print(fetch_jobs.__name__) # fetch_jobs (а не wrapper — благодаря @wraps)
Декоратор с параметрами
import functools
import time
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
def retry(times: int = 3, delay: float = 1.0, exceptions: tuple[type[Exception], ...] = (Exception,)):
def decorator(fn: F) -> F:
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last_exc: Exception | None = None
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except exceptions as exc:
last_exc = exc
print(f"[retry] {fn.__name__} attempt {attempt}/{times}: {exc}")
if attempt < times:
time.sleep(delay)
raise last_exc # type: ignore[misc]
return wrapper # type: ignore[return-value]
return decorator
@retry(times=3, delay=0.1, exceptions=(ConnectionError, TimeoutError))
def call_external_api(url: str) -> dict:
raise ConnectionError("connection refused")
Декоратор-класс
import functools
from typing import Any, Callable
class memoize:
"""Кешировать результат по аргументам."""
def __init__(self, fn: Callable[..., Any]) -> None:
functools.update_wrapper(self, fn)
self._fn = fn
self._cache: dict[tuple, Any] = {}
def __call__(self, *args: Any, **kwargs: Any) -> Any:
key = (args, tuple(sorted(kwargs.items())))
if key not in self._cache:
self._cache[key] = self._fn(*args, **kwargs)
return self._cache[key]
@memoize
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35)) # быстро, без рекурсивных перевычислений
Стекирование декораторов
@timer
@retry(times=2)
def load_data(source: str) -> list[str]:
...
# Эквивалентно:
# load_data = timer(retry(times=2)(load_data))
# Порядок применения снизу вверх: сначала retry, потом timer
Встроенные декораторы Python
import functools
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
@property
def area(self) -> float: # @property — геттер без скобок
return 3.14159 * self.radius ** 2
@staticmethod
def validate(r: float) -> bool: # не получает self/cls
return r > 0
@classmethod
def unit(cls) -> "Circle": # получает cls вместо self
return cls(1.0)
@functools.lru_cache(maxsize=128) # встроенное мемоизирование
def expensive(n: int) -> int:
return sum(range(n))
Подводные камни
- Без
@functools.wrapsдекорированная функция теряет__name__,__doc__,__annotations__— ломается introspection, sphinx-документация и некоторые фреймворки (FastAPI читает аннотации). - Декоратор без скобок (
@retry) и декоратор со скобками (@retry()) — разные вызовы; первый передаёт функцию напрямую, второй сначала вызываетretry(). - Стекирование порядка декораторов не интуитивно: декоратор, написанный ближе к функции, применяется первым.
- Декоратор-класс ломает методы:
selfне передаётся корректно без реализации__get__(дескриптор). - Декораторы с изменяемым состоянием (как счётчик вызовов) не thread-safe без Lock.
@functools.lru_cacheна методах экземпляра удерживаетselfв кеше, что предотвращает garbage collection объекта.
Common mistakes
- Описывать decorators только как термин и не показывать механизм на минимальном примере.
- Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
- Не связывать поведение с официальным контрактом Python и реальной эксплуатацией.
What the interviewer is testing
- Объясняет decorators через последовательность действий, а не через набор ключевых слов.
- Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.