Декораторы

⏱ 35 мин · 🧩 заданий: 5

Идея

Декоратор — функция, которая принимает функцию и возвращает функцию (обычно обёртку вокруг исходной). Так можно добавить поведение — логирование, замер времени, проверку прав — не меняя код самой функции.

def loud(func):
    def wrapper():
        print(">>> перед вызовом")
        result = func()
        print("<<< после вызова")
        return result
    return wrapper

def hello():
    print("Привет!")

hello = loud(hello)      # вручную «декорируем»
hello()

Синтаксис @ — сокращение для того же самого:

def loud(func):
    def wrapper():
        print(">>> перед вызовом")
        result = func()
        print("<<< после вызова")
        return result
    return wrapper

@loud                    # то же, что hello = loud(hello)
def hello():
    print("Привет!")

hello()

Пройдите пример 👣 Пошагово: при вызове hello() на самом деле вызывается wrapper, который вызывает исходную функцию через замыкание.

Универсальная обёртка

Чтобы декоратор подходил к функциям с любыми аргументами, обёртка принимает *args, **kwargs и пробрасывает их:

import functools
import time

def timer(func):
    @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 * 1000:.2f} мс")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(i * i for i in range(n))

print(slow_sum(200_000))
print(slow_sum.__name__)

functools.wraps

Без wraps обёртка «подменяет» метаданные функции:

import functools

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def proper(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def a():
    """Документация a."""

@proper
def b():
    """Документация b."""

print(a.__name__, a.__doc__)
print(b.__name__, b.__doc__, b.__wrapped__)

wraps копирует __name__, __doc__, __module__, __qualname__, аннотации и добавляет __wrapped__ — ссылку на исходную функцию. Это важно для отладки, логов, документации и фреймворков (например, pytest и FastAPI смотрят на сигнатуру).

Правило

Всегда ставьте @functools.wraps(func) на обёртку. На собеседовании это показывает, что вы писали декораторы «по-настоящему».

Несколько декораторов

Применяются снизу вверх (ближайший к функции — первым), а при вызове обёртки выполняются сверху вниз:

def tag(name):
    def deco(func):
        def wrapper():
            return f"<{name}>{func()}</{name}>"
        return wrapper
    return deco

@tag("b")
@tag("i")
def text():
    return "привет"

print(text())    # то же, что tag("b")(tag("i")(text))()

Когда выполняется декоратор

def register(func):
    print(f"регистрирую {func.__name__}")
    return func

@register
def f():
    print("вызов f")

print("--- функции определены ---")
f()
f()

Тело декоратора выполняется один раз при определении функции (при импорте модуля), а обёртка — при каждом вызове.

Практические примеры

Регистрация обработчиков

Так устроены маршруты во Flask и FastAPI (@app.get("/")), команды CLI, обработчики событий:

handlers = {}

def on(event):
    def deco(func):
        handlers[event] = func
        return func          # функцию не оборачиваем — просто запоминаем
    return deco

@on("start")
def start():
    return "Запуск!"

@on("stop")
def stop():
    return "Остановка"

print(handlers["start"](), handlers["stop"](), list(handlers))

Проверка прав

import functools

def require_admin(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_admin"):
            raise PermissionError(f"{user['name']}: нет прав на {func.__name__}")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_post(user, post_id):
    return f"Пост {post_id} удалён пользователем {user['name']}"

print(delete_post({"name": "admin", "is_admin": True}, 7))
try:
    delete_post({"name": "guest"}, 7)
except PermissionError as e:
    print("Ошибка:", e)

Логирование вызовов

import functools

def trace(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        params = ", ".join([repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()])
        result = func(*args, **kwargs)
        print(f"{func.__name__}({params}) = {result!r}")
        return result
    return wrapper

@trace
def add(a, b=0):
    return a + b

add(2, 3)
add(5, b=10)

Встроенные декораторы

Вы будете постоянно встречать: @property, @classmethod, @staticmethod (ООП), @functools.lru_cache и @functools.cache (кеширование), @dataclass, @contextlib.contextmanager, @pytest.fixture, @app.get(...).

Итог

  • Декоратор: func → новая func; @deco над def f равно f = deco(f).
  • Обёртка принимает *args, **kwargs, вызывает исходную функцию и возвращает её результат.
  • @functools.wraps(func) сохраняет имя, docstring и __wrapped__.
  • Несколько декораторов применяются снизу вверх; тело декоратора выполняется один раз при определении.
  • Применения: тайминг, логирование, права, повторы, регистрация, кеширование.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите декоратор shout, который переводит результат декорируемой функции (строку) в верхний регистр.

Какие тесты проверяют код (2)
assert greet("аня") == "ПРИВЕТ, АНЯ"
assert greet(name="боря") == "ПРИВЕТ, БОРЯ", "Обёртка должна пробрасывать именованные аргументы"
Задание 2 Программа на Python

Напишите декоратор count_calls, который считает вызовы функции и хранит число в атрибуте обёртки calls. Используйте functools.wraps, чтобы сохранить имя функции.

Какие тесты проверяют код (2)
@count_calls
def ping():
    return "pong"
ping(); ping(); ping()
assert ping.calls == 3

@count_calls
def my_func():
    "Документация"
assert my_func.__name__ == "my_func", "Используйте functools.wraps"
assert my_func.__doc__ == "Документация"

Задание 3 Один вариант ответа

Чему эквивалентен код?

@a
@b
def f(): ...
Задание 4 Программа на Python

Напишите декоратор retry, который при исключении повторяет вызов функции ещё до 2 раз (всего до 3 попыток). Если все попытки неудачны — пробросить последнее исключение.

Какие тесты проверяют код (2)
state = {"n": 0}
@retry
def flaky():
    state["n"] += 1
    if state["n"] < 3:
        raise ConnectionError("сбой")
    return "ok"
assert flaky() == "ok" and state["n"] == 3

@retry
def always_fails():
    raise ValueError("нет")
try:
    always_fails()
    raised = False
except ValueError:
    raised = True
assert raised, "После 3 неудачных попыток исключение должно пробрасываться"

Задание 5 Один вариант ответа

Когда выполняется код самого декоратора (не обёртки)?

Войдите, чтобы отмечать пройденные уроки.