Django-Admin.ru

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

Декораторы в Python: синтаксис @, обёртка с *args и **kwargs, functools.wraps, декоратор с параметрами, порядок применения, cache, примеры и частые ошибки.

Редакция Django-Admin.ru
· обновлено · 9 мин чтения

Содержание · 11 разделов
  1. На чём держатся декораторы
  2. Универсальный декоратор: *args, **kwargs и return
  3. functools.wraps: сохранить имя и docstring
  4. Декоратор с параметрами
  5. Пример: повтор при ошибке (retry)
  6. Несколько декораторов: порядок применения
  7. Логирование вызовов
  8. Готовые декораторы
  9. Стандартная библиотека
  10. Django и FastAPI
  11. Декоратор-класс
  12. Частые ошибки
  13. wrapper не возвращает результат
  14. Декоратор не возвращает wrapper
  15. Декоратор с параметрами без скобок
  16. Потерянное имя функции
  17. Обёртка принимает фиксированные аргументы
  18. Частые вопросы
  19. Как вызвать исходную функцию без декоратора?
  20. Как написать декоратор для async-функции?
  21. Как сделать декоратор, который работает и со скобками, и без?
  22. Можно ли декорировать класс?
  23. Как применить декоратор к функции из чужой библиотеки?
  24. Итог

Декоратор в Python — это функция, которая принимает другую функцию и возвращает новую, обычно с дополнительным поведением. Запись @decorator над def — сокращение для func = decorator(func). Короткий ответ:

def loud(func):
    def wrapper():
        print("До вызова")
        func()
        print("После вызова")
    return wrapper

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

hello()
До вызова
Привет!
После вызова

Декораторы позволяют добавить к функции логирование, замер времени, кэширование или проверку прав, не меняя её код. Ниже — как они устроены, как правильно написать свой декоратор, как передать ему параметры и какие декораторы уже есть в стандартной библиотеке, Django и FastAPI. Примеры проверены на Python 3.12.

На чём держатся декораторы

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

  1. Функция — обычный объект. Её можно передать в другую функцию как аргумент и вернуть как результат.
  2. Замыкание. Вложенная функция помнит переменные объемлющей функции — wrapper помнит func даже после того, как loud завершилась.

Без синтаксиса @ тот же код выглядит так:

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

hello = loud(hello)   # то же, что @loud над def
hello()
print(hello.__name__)  # → wrapper

Имя hello теперь указывает на wrapper, а исходная функция доступна только внутри замыкания. Последняя строка показывает побочный эффект, к которому мы вернёмся ниже.

Универсальный декоратор: *args, **kwargs и return

Декоратор loud работает только с функциями без аргументов и теряет возвращаемое значение. Правильный шаблон принимает любые аргументы через *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:.2f} с")
        return result
    return wrapper

@timer
def slow_sum(a, b, delay=0.5):
    time.sleep(delay)
    return a + b

print(slow_sum(2, 3))
slow_sum выполнилась за 0.50 с
5

Время на вашей машине может немного отличаться. Этот шаблон — основа почти любого декоратора: получить функцию, объявить wrapper, вызвать в нём func(*args, **kwargs), вернуть результат, вернуть wrapper.

functools.wraps: сохранить имя и docstring

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

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

@timer
def slow_sum(a, b):
    """Сложить два числа."""
    return a + b

print(slow_sum.__name__, slow_sum.__doc__)  # → wrapper None

С @functools.wraps(func) над wrapper вывод станет slow_sum Сложить два числа.. Декоратор wraps копирует __name__, __doc__, __module__ и другие атрибуты и добавляет __wrapped__ — ссылку на исходную функцию. Это важно для отладки, документации, help() и фреймворков, которые читают имя и сигнатуру функции. Используйте wraps всегда.

Декоратор с параметрами

Чтобы передать декоратору настройки — @repeat(times=3), — нужен ещё один уровень вложенности: внешняя функция принимает параметры и возвращает настоящий декоратор:

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping(host):
    print("ping", host)

ping("example.com")
ping example.com
ping example.com
ping example.com

Запись @repeat(times=3) сначала вызывает repeat(3), получает decorator, и уже он применяется к ping.

Пример: повтор при ошибке (retry)

import functools

def retry(attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"Попытка {attempt} не удалась: {e}")
                    if attempt == attempts:
                        raise
        return wrapper
    return decorator

calls = 0

@retry(attempts=3, exceptions=(ConnectionError,))
def fetch():
    global calls
    calls += 1
    if calls < 3:
        raise ConnectionError("сервер не отвечает")
    return "данные"

print(fetch())
Попытка 1 не удалась: сервер не отвечает
Попытка 2 не удалась: сервер не отвечает
данные

Перехватываются только перечисленные типы исключений, а после последней попытки ошибка пробрасывается дальше через raise. Как работают try/except и raise, описано в статье об обработке исключений.

Несколько декораторов: порядок применения

Декораторы применяются снизу вверх — ближайший к def первым:

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold
@italic
def text():
    return "Привет"

print(text())  # → <b><i>Привет</i></b>

Это эквивалентно text = bold(italic(text)): italic оборачивает функцию первым, а bold — снаружи. При вызове внешний декоратор срабатывает первым.

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

Ещё один типичный пример — печать аргументов и результата:

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        params = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
        print(f"→ {func.__name__}({', '.join(params)})")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} вернула {result!r}")
        return result
    return wrapper

@log_calls
def greet(name, punctuation="!"):
    return f"Привет, {name}{punctuation}"

greet("Анна", punctuation="?")
→ greet('Анна', punctuation='?')
← greet вернула 'Привет, Анна?'

Готовые декораторы

Стандартная библиотека

ДекораторЧто делает
@propertyметод, который читается как атрибут
@classmethodметод получает класс cls вместо объекта
@staticmethodметод без self и cls
@functools.cacheзапоминает результаты вызовов без ограничения размера (3.9+)
@functools.lru_cache(maxsize=128)кэш с вытеснением давно не использованных значений
@functools.wraps(func)переносит имя и docstring на обёртку
@dataclasses.dataclassгенерирует __init__, __repr__, __eq__ для класса
@contextlib.contextmanagerпревращает генератор в контекстный менеджер для with

Первые три подробно разобраны в статье о классах и ООП в Python. Кэш наглядно ускоряет рекурсию:

from functools import cache

@cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(80))           # → 23416728348467685
print(fib.cache_info())  # → CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)

Без @cache такой вызов пересчитывал бы одни и те же значения экспоненциальное число раз. Кэшировать стоит только функции, результат которых зависит лишь от аргументов, а аргументы должны быть хешируемыми.

Django и FastAPI

Во фреймворках декораторы встречаются на каждом шаге:

  • Django: @login_required из django.contrib.auth.decorators закрывает представление от анонимных пользователей, @admin.register(Article) регистрирует модель в админке — см. руководство по Django Admin.
  • FastAPI: @app.get("/items/") связывает функцию с маршрутом — подробнее в руководстве по FastAPI.

Декоратор вроде admin.register показывает ещё одну особенность: декоратор выполняется один раз, в момент объявления функции или класса, а не при каждом вызове. Поэтому его удобно использовать для регистрации:

registry = []

def register(func):
    registry.append(func.__name__)
    return func          # возвращаем функцию без изменений

@register
def export_csv(): ...

@register
def export_json(): ...

print(registry)  # → ['export_csv', 'export_json']

Декоратор-класс

Декоратором может быть любой вызываемый объект, в том числе класс с методом __call__. Так удобно хранить состояние между вызовами:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def hello():
    return "hi"

hello()
hello()
print(hello.calls)  # → 2

Частые ошибки

wrapper не возвращает результат

def broken(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)        # нет return
    return wrapper

@broken
def add(a, b):
    return a + b

print(add(2, 3))  # → None

Пишите return func(*args, **kwargs), иначе декорированная функция всегда возвращает None.

Декоратор не возвращает wrapper

Если забыть return wrapper в конце декоратора, имя функции будет указывать на None, и вызов add(2, 3) упадёт с TypeError: 'NoneType' object is not callable.

Декоратор с параметрами без скобок

Если написать @repeat вместо @repeat(3), функция ping попадёт в параметр times, а имя ping будет указывать на внутренний decorator. Вызов ping("example.com") ничего не напечатает и молча вернёт объект функции, а ping() без аргументов упадёт с TypeError: repeat.<locals>.decorator() missing 1 required positional argument: 'func'. Декоратор с параметрами всегда вызывается со скобками.

Потерянное имя функции

Без functools.wraps у всех декорированных функций __name__ равно wrapper. Это ломает логи, документацию и фреймворки, которые различают функции по имени.

Обёртка принимает фиксированные аргументы

def wrapper(): работает только для функций без параметров. Используйте *args, **kwargs, чтобы декоратор подходил для любой функции, в том числе для методов класса: self просто придёт первым элементом args.

Частые вопросы

Как вызвать исходную функцию без декоратора?

Если обёртка создана с functools.wraps, исходная функция лежит в атрибуте __wrapped__. Это удобно в тестах, когда кэш, логирование или проверка прав мешают:

print(slow_sum.__wrapped__(2, 3))  # → 5, без замера времени

Без wraps атрибута __wrapped__ нет, и добраться до исходной функции снаружи нельзя.

Как написать декоратор для async-функции?

Обёртка тоже должна быть корутиной: объявите её через async def и дождитесь результата через await:

import asyncio
import functools
import time

def async_timer(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = await func(*args, **kwargs)
        print(f"{func.__name__}: {time.perf_counter() - start:.1f} с")
        return result
    return wrapper

@async_timer
async def load():
    await asyncio.sleep(0.2)
    return "ok"

print(asyncio.run(load()))  # → load: 0.2 с, затем ok

Обычный синхронный wrapper вернёт корутину без await, и замер времени окажется бессмысленным.

Как сделать декоратор, который работает и со скобками, и без?

Первым параметром примите функцию со значением None, а настройки сделайте только именованными. Если декоратор вызван со скобками, функции ещё нет — верните его же с подставленными настройками:

import functools

def repeat(func=None, *, times=2):
    if func is None:
        return functools.partial(repeat, times=times)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for _ in range(times):
            result = func(*args, **kwargs)
        return result
    return wrapper

@repeat            # 2 раза
def a(): print("a")

@repeat(times=3)   # 3 раза
def b(): print("b")

Можно ли декорировать класс?

Да, декоратор получает класс так же, как функцию, и возвращает класс — изменённый или новый. Так работают @dataclass и @admin.register(Article) в Django. Синтаксис тот же: @decorator над строкой class.

Как применить декоратор к функции из чужой библиотеки?

Вызовите декоратор вручную, без @: cached_get = functools.cache(some_lib.get). Исходная функция при этом не меняется, обёртка доступна под новым именем.

Итог

Декоратор — функция, которая принимает функцию и возвращает обёртку; @name над def — краткая форма func = name(func). Правильный шаблон: wrapper(*args, **kwargs), return func(*args, **kwargs), @functools.wraps(func); для параметров добавьте внешний уровень вложенности. Короткие функции, которые передают в другие функции, разобраны в статье о lambda в Python, а весь базовый синтаксис собран в шпаргалке по синтаксису Python.

  • #Python
  • #Декораторы
  • #functools
  • #Функции
  • #Синтаксис
↑ ↓ выборEnter открытьEsc закрыть