Декораторы в Python
Допустим, у нас есть несколько функций и мы хотим во всех замерить время выполнения. Можно скопировать код тайминга в каждую:
Python 3.13def slow_function(): start = time.time() # ... основная работа elapsed = time.time() - start print(f"slow_function: {round(elapsed, 4)} c") def another_function(): start = time.time() # ... основная работа elapsed = time.time() - start print(f"another_function: {round(elapsed, 4)} c")
Работает, но через неделю мы захотим поменять формат логирования, и придётся редактировать все функции по очереди. Плюс основная логика теряется в служебном коде.
Декораторы решают именно это: вы пишете обёртку один раз, и навешиваете её на любую функцию короткой строкой @обёртка над определением.
Функция — это значение
Сначала одна ступенька, без которой декораторы не понять. Функция в Python — такое же значение, как число или строка: её можно положить в переменную, передать в другую функцию и вернуть из функции.
Python 3.13def say_hello(): print("Привет!") greet = say_hello # без скобок: берём саму функцию, а не её результат greet()Привет!
say_hello без скобок — сама функция, со скобками — её вызов. Передавать функцию вы уже пробовали в прошлом уроке: sorted(key=lambda ...) — это функция, отданная в другую функцию. Осталось третье: функцию можно объявить внутри другой и вернуть наружу — именно так декоратор и устроен.
Базовый синтаксис
Декоратор — функция, которая берёт другую функцию и возвращает её «обёрнутую» версию (с добавленным поведением):
Python 3.13def my_decorator(func): def wrapper(): print("До вызова функции") func() print("После вызова функции") return wrapper @my_decorator def say_hello(): print("Привет, мир!") say_hello()До вызова функции Привет, мир! После вызова функции
Запись @my_decorator над say_hello — это сокращение для такой строки:
Python 3.13say_hello = my_decorator(say_hello)
То есть имя say_hello теперь указывает на новую функцию, которую вернул декоратор, — обычное переприсваивание.
Декоратор с аргументами функции
Если оборачиваемая функция принимает аргументы, обёртка должна их пробрасывать. Универсальный приём: *args, **kwargs:
Python 3.13def my_decorator(func): def wrapper(*args, **kwargs): print("До вызова") result = func(*args, **kwargs) print("После вызова") return result return wrapper @my_decorator def add(a, b): return a + b print(add(5, 3))До вызова После вызова 8
*args, **kwargs означает «приму любые позиционные и именованные аргументы», и func(*args, **kwargs) пробрасывает их дальше. Этим приёмом декоратор становится универсальным — работает с любой функцией.
Заметьте порядок вывода: 8 печатается последним, потому что внешний print ждёт, пока обёртка отработает целиком и вернёт результат.
Практический декоратор: тайминг
Тот самый тайминг, ради которого мы всё начали. Одна деталь: у каждой функции есть атрибут __name__ с её именем — обёртка подпишет им замер:
Python 3.13import time def timing(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) elapsed = time.time() - start print(f"{func.__name__}: {round(elapsed, 4)} c") return result return wrapper @timing def calculate_sum(n): return sum(range(n)) calculate_sum(1_000_000)calculate_sum: 0.0462 c
Теперь добавить тайминг к любой функции — одна строка @timing сверху. Захотим поменять формат вывода: правим одну функцию timing, а не каждую функцию в проекте.
functools.wraps: сохранение имени функции
У наивного декоратора есть незаметный побочный эффект: обёрнутая функция «теряет» своё имя, потому что снаружи вы видите уже wrapper, а не оригинал:
Python 3.13def timing(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @timing def calculate_sum(n): return sum(range(n)) print(calculate_sum.__name__)wrapper
В реальном коде это ломает отладку и сообщения об ошибках. Лечится одной строкой — декоратором @functools.wraps(func) на wrapper:
Python 3.13from functools import wraps def timing(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @timing def calculate_sum(n): return sum(range(n)) print(calculate_sum.__name__)calculate_sum
Правило: пишете свой декоратор — всегда оборачивайте внутреннюю функцию через @wraps(func).
Декоратор с параметрами
Иногда хочется передать настройки самому декоратору, например «повтори вызов N раз». Это требует ещё одного уровня: внешняя функция принимает параметр, внутри возвращает «настоящий» декоратор:
Python 3.13from functools import wraps def repeat(n=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(n=3) def say_hi(name): print(f"Привет, {name}!") say_hi("Анна")Привет, Анна! Привет, Анна! Привет, Анна!
Три уровня вложенности кажутся пугающими, но логика простая:
- repeat(n) принимает параметр декоратора и возвращает обычный декоратор.
- decorator(func) принимает функцию и возвращает обёртку.
- wrapper(*args, **kwargs) обрабатывает реальный вызов.
В развёрнутом виде — тот же приём, что и раньше, только с одним лишним вызовом:
Python 3.13say_hi = repeat(n=3)(say_hi)
Сначала выполняется обычный вызов repeat(n=3) — он возвращает настоящий декоратор, и тот сразу применяется к say_hi.
Имя _ в цикле — соглашение: переменная нужна только чтобы цикл крутился, её значение не используется.
Где декораторы живут в реальности
Несколько мест, где вы их встретите чаще всего:
Веб-фреймворки. Привязка адреса страницы к функции-обработчику — например, во Flask или FastAPI:
Python 3.13@app.route('/home') def home(): return "Главная страница"
@app.route регистрирует функцию в маршрутизаторе фреймворка: запросил браузер /home — фреймворк вызовет home().
Кэширование. Декоратор @lru_cache из functools запоминает результаты функции: повторный вызов с теми же аргументами ничего не пересчитывает, а сразу возвращает готовый ответ.
Тесты. В pytest декораторы @pytest.fixture и @pytest.mark.parametrize превращают обычную функцию в заготовку данных или целую серию тестов — встретите их в третьем модуле курса.
Проверка понимания
В чём основное назначение декораторов в Python?
Декораторы — рабочая лошадка Python: на них строятся веб-фреймворки, тесты и уже знакомый вам @property. Когда вы знаете, что @что-то — это просто func = что-то(func), читать такой код становится сильно проще.
В следующем уроке — работа с датами и временем: модуль datetime, разница дат и их форматирование.
