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-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics