Декораторы
Идея
Декоратор — функция, которая принимает функцию и возвращает функцию (обычно обёртку вокруг исходной). Так можно добавить поведение — логирование, замер времени, проверку прав — не меняя код самой функции.
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
Напишите декоратор shout, который переводит результат декорируемой функции (строку) в верхний регистр.
Какие тесты проверяют код (2)
assert greet("аня") == "ПРИВЕТ, АНЯ"
assert greet(name="боря") == "ПРИВЕТ, БОРЯ", "Обёртка должна пробрасывать именованные аргументы"
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f"привет, {name}"
Напишите декоратор 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__ == "Документация"
import functools
def count_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
Чему эквивалентен код?
@a
@b
def f(): ...
Напишите декоратор 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 неудачных попыток исключение должно пробрасываться"
import functools
def retry(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(3):
try:
return func(*args, **kwargs)
except Exception:
if attempt == 2:
raise
return wrapper
Когда выполняется код самого декоратора (не обёртки)?
Войдите, чтобы отмечать пройденные уроки.