Python декораторы: что это и как работают

Python декораторы: что это и как работают
0
15

Python декораторы — это инструмент, который позволяют обрабатывать функцию или класс при определении. Например, добавлять поведение, регистрировать объект или заменять его другим без изменения тела исходного определения. Чтобы разобраться, что такое декоратор в Python, достаточно понять, как функции передаются между объектами и почему одна функция может возвращать другую.

Python декораторы: что это и как работают

Для чего нужны декораторы в Python

Основное назначение декораторов связано с принципом DRY (Don’t Repeat Yourself), то есть устранением повторяющегося кода. Если нескольким обработчикам API требуется одинаковая проверка доступа, её удобно вынести в отдельный декоратор.

Без него проверка будет выглядеть примерно так:

def get_profile(user):
    if not user.is_authenticated:
        raise PermissionError()
    return user.profile

def get_orders(user):
    if not user.is_authenticated:
        raise PermissionError()
    return user.orders

При изменении правил придётся обновлять каждую функцию. Декоратор позволяет переиспользовать одну реализацию:

from functools import wraps

def login_required(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.is_authenticated:
            raise PermissionError()
        return func(user, *args, **kwargs)
    return wrapper

@login_required
def get_profile(user):
    return user.profile

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

  • логирование вызовов и обработку ошибок;
  • кэширование результатов;
  • проверку прав доступа;
  • измерение производительности;
  • регистрацию обработчиков и маршрутов.
★ Где этому научиться

Курс «Python Ninja» в Академии IT DMS

✓Основы Python с нуля: от переменных до ООП
✓Веб-приложения на Flask и Django, свои API
✓SQL, графические интерфейсы и машинное обучение на практике

Сертификат, диплом о переподготовке или удостоверение о повышении квалификации.

Перейти к курсу →ПромокодAFFTIMES

−5 % на первую покупку. Скидка суммируется с любыми акциями школы. Если оплачиваете через менеджера в чате, назовите ему промокод.

Как работают декораторы в Python

Функции как объекты первого класса

В Python функции являются объектами первого класса, их можно передавать в аргументах, присваивать переменным и возвращать из других функций. Функции относятся к вызываемым объектам — callable. 

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

def execute(func):
    return func()

result = execute(hello)

Здесь execute получает функцию hello и вызывает её. Такой принцип лежит в основе декораторов.

Декоратор технически представляет вызываемый объект, который принимает декорируемый объект и возвращает результат преобразования. Чаще всего это функция, принимающая функцию и возвращающая новую функцию.

Синтаксис @ и его эквивалент

Знак @ перед определением функции обозначает применение декоратора. Следующие варианты эквивалентны:

@decorator
def func():
    return 10

# Эквивалентная запись:
func = decorator(func)

Python сначала создаёт функцию func, затем передаёт её декоратору. Имя func связывается с возвращённым объектом.

При обычном декорировании сам декоратор вызывается во время определения функции, а обёртка выполняется при последующих вызовах.

Простейший декоратор и замыкание

def announce(func):
    def wrapper():
        print("Вызов функции")
        return func()
    return wrapper

@announce
def hello():
    return "Привет"

print(hello())

wrapper получает доступ к func, объявленной во внешней функции. Такая связь называется замыканием (closure). Обёртка сохраняет ссылку на исходный объект и использует её при вызове.

Python декораторы: что это и как работают

Для универсального декоратора требуется поддержка аргументов:

def announce(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

*args собирает позиционные аргументы, а **kwargs принимает именованные. Если их не предусмотреть, вызов метода с дополнительными параметрами может завершиться TypeError. Такая ошибка часто появляется после подключения декоратора к уже работающему коду.

Декораторы с аргументами и без

ЭлементБез аргументовС аргументами
Внешняя функцияdecorator(func)factory(option)
Средняя функцияwrapper(*args, **kwargs)decorator(func)
Внутренняя функцияВызывает funcwrapper(*args, **kwargs)
Применение@decorator@factory(option)

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

Фабрика декораторов

Когда нужно настроить поведение, используют фабрику декораторов (decorator factory). Например, можно добавить несколько повторных попыток:

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

@repeat(3)
def greet():
    print("Привет")

Пример рассчитан на положительное целое times. Обёртка возвращает результат последнего вызова и прекращает работу при первом исключении.

Здесь три уровня:

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

Сохранение метаданных через functools.wraps

Обёртка без дополнительных настроек получает собственные метаданные. Например, __name__ будет содержать wrapper, а __doc__ может оказаться равным None.

Декоратор functools.wraps помогает сохранить информацию об исходной функции:

from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

wraps переносит на обёртку основные атрибуты исходной функции, включая:

  • __name__ — имя;
  • __doc__ — строку документации;
  • __module__ — имя модуля;
  • __wrapped__ — ссылку на обёрнутый объект.

Также сохраняются аннотации и другие предусмотренные механизмом атрибуты.

Множественные декораторы

К одной функции можно применить несколько модификаторов:

@decorator_a
@decorator_b
def process():
    return 1

Эквивалентная запись:

process = decorator_a(decorator_b(process))

Python декораторы: что это и как работают

Порядок применения и выполнения различается.

  1. Сначала применяется нижний декоратор decorator_b.
  2. Его результат передаётся в decorator_a.
  3. При вызове управление сначала попадает во внешнюю обёртку decorator_a.
  4. Затем выполняется decorator_b, а после него исходная функция.

Таким образом, применение идёт снизу вверх, а вход в обёртки при вызове происходит сверху вниз.

Декораторы классов

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

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

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

@CallCounter
def calculate(x):
    return x * 2

__init__ сохраняет исходный callable, а __call__ определяет поведение экземпляра при вызове. Такой вариант подходит, когда нужно хранить состояние между обращениями.

В примере выше calculate становится экземпляром CallCounter. При декорировании методов такой объект не реализует автоматически протокол дескриптора, поэтому обычное связывание self работать не будет.

КритерийДекоратор-функцияДекоратор-класс
РеализацияВложенная функция__init__ и __call__
СостояниеЗамыканиеАтрибуты экземпляра
ПрименениеЛогирование, таймерСчётчики, состояние
ОсобенностиКомпактностьНужен учёт дескрипторов и метаданных

Для декорирования самих классов тоже используют @. Например, @dataclass преобразует описание класса, создавая ряд методов.

Асинхронный декоратор

Для корутин нужна асинхронная обёртка:

from functools import wraps

def async_log(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        print("Старт")
        result = await func(*args, **kwargs)
        return result
    return wrapper

await нужен, чтобы дождаться результата корутины. Если вызвать func(*args, **kwargs) без ожидания, получится объект корутины, а не готовое значение.

Python декораторы: что это и как работают

Обычная синхронная обёртка может вызвать async-функцию и вернуть корутину, но тогда код после вызова выполнится до завершения асинхронной операции. Для асинхронных функций следует применять отдельный декоратор с async def и await.

Типизация декораторов Python

Callable, TypeVar и ParamSpec

Для аннотации декораторов используют typing.Callable, TypeVar и ParamSpec. В Python 3.10 доступны ParamSpec и Concatenate.

Пример типизированного декоратора:

from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def logged(func: Callable[P, R]) -> Callable[P, R]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("Вызов функции")
        return func(*args, **kwargs)
    return wrapper

P описывает параметры исходной функции, а R обозначает тип результата. Благодаря этому анализатор типов сохраняет информацию о сигнатуре.

Callable[…, R] тоже допустим, но он не сохраняет конкретные типы параметров так точно, как ParamSpec. При этом Callable не описывает произвольную сигнатуру во всех деталях, например сложные перегрузки могут требовать дополнительных аннотаций.

Для расширения профессиональных навыков Python-разработчика изучате продвинутые возможности Python, включая работу с типами и архитектурой приложений.

Практические рекомендации

При написании собственных декораторов учитывайте несколько правил:

  • Используйте @wraps(func) для сохранения метаданных.
  • Передавайте *args и **kwargs, если декоратор должен работать с разными сигнатурами.
  • Возвращайте результат исходной функции, если контракт не предполагает другого.
  • Для async-кода используйте корректную асинхронную обёртку.
  • Не храните изменяемые настройки в аргументах по умолчанию фабрики без необходимости.

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

Практические примеры использования декораторов

Декоратор замера времени

Для оценки продолжительности выполнения функции можно использовать time.perf_counter():

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            print(f"{func.__name__}: {elapsed:.6f} сек.")
    return wrapper

@timer
def calculate():
    return sum(range(1_000_000))

calculate()

Замер учитывает время выполнения вызова, включая исключения. Однако он также включает ожидание внешних ресурсов, если функция блокируется на вводе-выводе.

Чтобы сравнить версии функции, можно выполнить несколько замеров без декоратора и с ним. Для честного сравнения нужны одинаковые входные данные, прогрев и серия запусков. Сам декоратор добавляет накладные расходы, поэтому для микробенчмарков лучше использовать timeit.

Кэширование через lru_cache

Стандартная библиотека functools содержит готовый декоратор @lru_cache, который сохраняет результаты вызовов:

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(30))
print(fibonacci.cache_info())

lru_cache (Least Recently Used) хранит результаты последних вызовов с учётом лимита maxsize. Он подходит для функций, результат которых зависит от аргументов и которые не имеют побочных эффектов.

Python декораторы: что это и как работают

Аргументы должны быть хешируемыми. Кэширование методов также требует учитывать self, который участвует в ключе. Если экземпляры долго живут в кэше, это может влиять на использование памяти.

Маршрутизация в веб-фреймворках

Декораторы широко используются во Flask, FastAPI и Django.

Во Flask запись @app.route(«/profile») регистрирует функцию в маршрутизаторе. Во FastAPI @app.get(«/users») связывает обработчик с HTTP-маршрутом и метаданными запроса.

В Django применяются декораторы для контроля доступа, например login_required. В CLI-инструментах Click декораторы помогают объявлять команды и параметры.

Отладка декорированных функций через inspect

При отладке сначала проверьте, какой объект привязан к имени функции:

import inspect

print(func.__name__)
print(inspect.signature(func))
print(inspect.unwrap(func))

inspect.signature() обычно использует сведения __wrapped__, если они доступны. inspect.unwrap() проходит по цепочке обёрток до исходного объекта.

Если результат отличается от ожидаемого, проверьте:

  • сохранил ли декоратор исходную функцию;
  • используется ли @wraps;
  • не изменена ли сигнатура намеренно;
  • соответствует ли обёртка синхронному или асинхронному вызову.

Для трассировки исключений смотрите traceback целиком. wraps сохраняет метаданные, но не переименовывает реальные кадры выполнения. Имя wrapper всё ещё может встречаться в стеке.

FAQ

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

Да. Декоратор класса получает объект класса и возвращает преобразованный объект. Например, @dataclass генерирует специальные методы.

Чем декоратор отличается от обычной функции-обёртки?

Обёртка реализует дополнительную логику вокруг вызова. Декоратор применяет такую обёртку к переданному callable и возвращает результат. Обёртка часто находится внутри декоратора.

Как передать аргументы в декорируемую функцию?

Передавайте их при обычном вызове: func(10, name="Alex"). Внутри универсальной обёртки используйте *args и **kwargs, чтобы передать значения исходной функции.

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

Да. Они применяются снизу вверх. Если один декоратор зависит от результата другого, порядок может влиять на поведение. Например, кэширование и логирование могут учитывать разное количество вызовов в зависимости от расположения.

Можно ли снять декоратор после применения?

Если декоратор использует functools.wraps, исходный объект часто доступен через func.__wrapped__. Это позволяет получить оригинальную функцию, но не отменяет уже выполненные побочные эффекты и не восстанавливает автоматически состояние приложения.

Влияют ли декораторы на производительность?

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

Лиля Райн
Лиля Райн
Автор
(Проголосовать за статью)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

X