Создание собственного декоратора в Python шаг за шагом

Как создать свой декоратор python

Как создать свой декоратор python

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

Первый шаг – определить функцию-декоратор, которая принимает функцию в качестве аргумента. Внутри неё создаётся вложенная функция, где реализуется дополнительная логика. После этого вложенная функция возвращается как результат работы декоратора. Такой подход сохраняет оригинальное поведение функции и добавляет новые возможности.

При проектировании декоратора важно учитывать передачу аргументов и возвращаемого значения. Для этого используется конструкция *args и **kwargs, позволяющая декоратору работать с функциями любого сигнатуры. Без этого декоратор будет ограничен только фиксированными параметрами.

Для отладки и сохранения метаданных исходной функции применяют модуль functools и функцию wraps. Она переносит имя функции, документацию и другие атрибуты, что важно при работе с реальными проектами и для корректного отображения информации при инспекции кода.

Следующие шаги статьи покажут, как шаг за шагом реализовать декоратор для измерения времени выполнения функций, проверки аргументов и логирования вызовов. Эти примеры демонстрируют, как универсальный декоратор может стать инструментом оптимизации и повышения прозрачности кода.

Определяем базовую функцию для декорирования

Определяем базовую функцию для декорирования

Базовая функция для декорирования должна быть простой и выполняться без побочных эффектов. Она принимает параметры, которые передаются оборачиваемой функции, и возвращает результат её выполнения. Стандартная сигнатура использует *args и **kwargs для универсальности:

def simple_function(*args, **kwargs):

def simple_function(*args, **kwargs):
  result = sum(args)
  return result

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

Рекомендуется добавлять аннотации типов для прозрачности и упрощения последующего использования функции в декораторе:

def simple_function(*args: int, **kwargs: int) -> int:

Такая подготовка позволяет создать надёжную основу для декоратора: функция легко заменяется и расширяется, а логика декорирования не ломает основной код.

Создание простой обертки с дополнительным действием

Создание простой обертки с дополнительным действием

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

def decorator(func):

    def wrapper(*args, **kwargs):

        print("Начало выполнения функции")

        result = func(*args, **kwargs)

        return result

    return wrapper

Чтобы применить декоратор, используем синтаксис @decorator перед определением функции:

@decorator

def greet(name):

    print(f"Привет, {name}!")

Вызов greet("Алиса") выведет:

Начало выполнения функции

Привет, Алиса!

Рекомендуется сохранять сигнатуру и документацию исходной функции. Для этого обертку оборачивают с помощью functools.wraps:

from functools import wraps

def decorator(func):

    @wraps(func)

    def wrapper(*args, **kwargs):

        print("Начало выполнения функции")

        return func(*args, **kwargs)

    return wrapper

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

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

Передача аргументов в декоратор и обертку

Чтобы декоратор принимал собственные аргументы, необходимо создать трёхуровневую структуру функций: внешняя функция получает аргументы декоратора, средняя – функцию для обёртки, внутренняя – аргументы исходной функции.

Пример шаблона:

def decorator_with_args(deco_arg1, deco_arg2):
def actual_decorator(func):
def wrapper(*args, **kwargs):
# Логика с использованием deco_arg1, deco_arg2
result = func(*args, **kwargs)
return result
return wrapper
return actual_decorator

Аргументы декоратора обрабатываются на уровне внешней функции и доступны внутри wrapper. Это позволяет изменять поведение обёртки без изменения исходной функции.

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

Уровень Принимаемые аргументы Использование
Внешний декоратор Аргументы декоратора Настройка логики wrapper
Средняя функция Функция, которую декорируем Оборачивает исходную функцию
Внутренняя функция (wrapper) Аргументы исходной функции Выполняет функцию и добавляет поведение

Важно использовать *args и **kwargs в wrapper для универсальности, чтобы декоратор корректно работал с любым количеством позиционных и именованных аргументов исходной функции.

Для передачи аргументов внутрь wrapper можно комбинировать значения декоратора и исходной функции. Например:

def repeat(times):
def decorator(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(name):
print(f"Hello, {name}!")

В этом примере аргумент times контролирует количество вызовов функции greet без изменения её сигнатуры.

Практическая рекомендация: всегда документируйте аргументы декоратора и сохраняйте __name__ и __doc__ функции через functools.wraps, чтобы не потерять метаданные исходной функции.

Использование functools.wraps для сохранения метаданных функции

Использование functools.wraps для сохранения метаданных функции

При создании собственного декоратора часто возникает проблема потери метаданных исходной функции: имени, строки документации и атрибутов. Это влияет на работу отладчиков, систем логирования и автодокументации. Решение – использование functools.wraps.

Принцип работы functools.wraps прост: он копирует атрибуты исходной функции в обёрнутую функцию. Стандартный синтаксис выглядит так:

from functools import wraps
def мой_декоратор(func):
@wraps(func)
def обёртка(*args, **kwargs):
# дополнительная логика
return func(*args, **kwargs)
return обёртка

Без @wraps(func) атрибуты функции заменяются атрибутами внутренней обёртки:

  • __name__ → «обёртка»
  • __doc__ → строка документации декоратора
  • Специальные атрибуты, такие как __annotations__ и __module__, теряются

Рекомендации по использованию:

  1. Импортируйте wraps только из functools, других реализаций нет.
  2. Применяйте @wraps непосредственно к внутренней функции обёртки, перед её определением логики.
  3. Если декоратор принимает параметры, wraps применяется к функции, возвращаемой вложенным декоратором.
  4. Для сложных случаев с несколькими уровнями обёртки используйте wraps на каждом уровне, чтобы сохранить цепочку метаданных.

Пример с параметрами декоратора:

def декоратор_с_параметром(prefix):
def декоратор(func):
@wraps(func)
def обёртка(*args, **kwargs):
print(f"{prefix}: вызов функции")
return func(*args, **kwargs)
return обёртка
return декоратор
@декоратор_с_параметром("INFO")
def функция_пример():
"""Документация функции"""
return 42

В результате:

  • функция_пример.__name__ → «функция_пример»
  • функция_пример.__doc__ → «Документация функции»

Использование functools.wraps обеспечивает прозрачность декораторов и предотвращает потерю критически важных метаданных.

Применение декоратора к методам классов

Применение декоратора к методам классов

Простейший пример декоратора для метода:

def log_call(func):
def wrapper(self, *args, **kwargs):
print(f"Вызов метода {func.__name__} с аргументами {args} {kwargs}")
return func(self, *args, **kwargs)
return wrapper
class Example:
@log_call
def greet(self, name):
return f"Привет, {name}!"

Здесь wrapper принимает self первым аргументом. Без этого метод не сможет корректно работать с экземпляром класса.

Для классовых методов (@classmethod) декоратор должен принимать cls:

def class_logger(func):
def wrapper(cls, *args, **kwargs):
print(f"Классовый метод {func.__name__} вызван с {args} {kwargs}")
return func(cls, *args, **kwargs)
return wrapper
class Example:
@classmethod
@class_logger
def info(cls, data):
return f"Информация: {data}"

Важно соблюдать порядок декораторов: сначала пользовательский декоратор, затем @classmethod, либо использовать functools.wraps, чтобы сохранить метаданные функции.

Для методов, которые должны быть статическими (@staticmethod), декоратор не получает автоматически self или cls. В этом случае wrapper должен принимать только те аргументы, которые реально используются.

Использование декораторов с методами классов позволяет централизованно добавлять логирование, проверку прав, кэширование или профилирование без изменения основного кода метода.

Комбинирование нескольких декораторов на одной функции

Комбинирование нескольких декораторов на одной функции

В Python можно накладывать несколько декораторов на одну функцию, размещая их в виде стека сверху вниз. Вызов выполняется в обратном порядке: сначала применяется внутренний декоратор, затем внешний. Например, если функция `func` обернута как `@d1 @d2 def func(): …`, сначала выполняется `d2(func)`, затем результат передается в `d1`.

При комбинировании важно учитывать передачу аргументов и возвращаемых значений. Каждый декоратор должен корректно обрабатывать `*args` и `**kwargs`, иначе внутренняя функция может получить некорректные данные. Для сохранения метаданных исходной функции используют `functools.wraps`, иначе `__name__`, `__doc__` и аннотации будут утеряны.

Порядок декораторов критичен при выполнении логики, зависящей от побочных эффектов. Например, если один декоратор логирует вызов, а другой кеширует результат, правильное расположение определяет, будет ли лог записан до или после кеширования.

Для сложных сценариев можно комбинировать декораторы с аргументами. Например, `@auth(role=’admin’) @timer def func(): …` сначала создаст обертку с таймером, затем добавит проверку роли. Важно, чтобы декораторы с параметрами возвращали функцию-обертку, иначе цепочка сломается.

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

Вопрос-ответ:

Что такое декоратор в Python и зачем он нужен?

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

Как создать простой декоратор для вывода сообщения перед и после вызова функции?

Для этого нужно определить функцию-декоратор, которая принимает функцию как аргумент и возвращает новую функцию. Внутри новой функции добавляются действия до и после вызова исходной функции. Например, внутри вложенной функции print можно вставить строки «Начало» и «Конец», а потом вызвать исходную функцию с переданными аргументами.

В чем разница между декоратором с аргументами и без аргументов?

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

Как сохраняется информация о исходной функции после применения декоратора?

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

Можно ли применять несколько декораторов к одной функции и как это делается?

Да, несколько декораторов можно накладывать друг на друга. Они применяются сверху вниз по записи. Например, если функция декорируется сначала @декоратор1, потом @декоратор2, то вызов функции сначала пройдет через декоратор2, а затем через декоратор1. Это позволяет комбинировать разные виды логики без изменения исходного кода функции.

Как правильно передавать аргументы в собственный декоратор в Python?

Чтобы декоратор корректно работал с функциями, принимающими аргументы, нужно использовать вложенную функцию с параметрами *args и **kwargs. Это позволяет обернутой функции принимать любое количество позиционных и именованных аргументов без ошибок. Сначала создаётся внешний декоратор, который принимает функцию, затем внутри него определяется обёртка, которая обрабатывает аргументы и выполняет дополнительные действия до или после вызова исходной функции. После этого декоратор возвращает обёртку вместо оригинальной функции. Такой подход обеспечивает гибкость и сохраняет возможность использовать декоратор с различными функциями.

Ссылка на основную публикацию