Декораторы в Python

Допустим, у нас есть несколько функций и мы хотим во всех замерить время выполнения. Можно скопировать код тайминга в каждую:

Python 3.13
def 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.13
def say_hello():
    print("Привет!")

greet = say_hello   # без скобок: берём саму функцию, а не её результат
greet()
Привет!

say_hello без скобок — сама функция, со скобками — её вызов. Передавать функцию вы уже пробовали в прошлом уроке: sorted(key=lambda ...) — это функция, отданная в другую функцию. Осталось третье: функцию можно объявить внутри другой и вернуть наружу — именно так декоратор и устроен.

Базовый синтаксис

Декоратор — функция, которая берёт другую функцию и возвращает её «обёрнутую» версию (с добавленным поведением):

Python 3.13
def my_decorator(func):
    def wrapper():
        print("До вызова функции")
        func()
        print("После вызова функции")
    return wrapper

@my_decorator
def say_hello():
    print("Привет, мир!")

say_hello()
До вызова функции
Привет, мир!
После вызова функции

Запись @my_decorator над say_hello — это сокращение для такой строки:

Python 3.13
say_hello = my_decorator(say_hello)

То есть имя say_hello теперь указывает на новую функцию, которую вернул декоратор, — обычное переприсваивание.

say_hello()
@my_decorator
wrapper()
print("До вызова")
say_hello()
print("После вызова")
Декоратор вернул новую функцию: внутри — исходная плюс код до и после

Декоратор с аргументами функции

Если оборачиваемая функция принимает аргументы, обёртка должна их пробрасывать. Универсальный приём: *args, **kwargs:

Python 3.13
def 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.13
import 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.13
def 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.13
from 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.13
from 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("Анна")
Привет, Анна!
Привет, Анна!
Привет, Анна!

Три уровня вложенности кажутся пугающими, но логика простая:

  1. repeat(n) принимает параметр декоратора и возвращает обычный декоратор.
  2. decorator(func) принимает функцию и возвращает обёртку.
  3. wrapper(*args, **kwargs) обрабатывает реальный вызов.

В развёрнутом виде — тот же приём, что и раньше, только с одним лишним вызовом:

Python 3.13
say_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, разница дат и их форматирование.