Как создать собственные Python-декораторы и правильно их использовать
Статья рассчитана на тех, кто владеет основами Python и хочет научиться создавать собственные декораторы для повышения качества кода.
7К открытий8К показов
Статья рассчитана на тех, кто владеет основами Python, знаком с декораторами и хочет научиться создавать собственные декораторы для повышения качества кода. Если вы забыли, что такое декораторы, — повторите тему по первым разделам статьи.
Михаил Залешин
К.ф.-м.н., Data Scientist, наставник и эксперт в Нетологии по курсам «Программист на JavaScript» и «Data Science: Рекомендательные системы»
Что такое декораторы в Python
Декоратор — конструкция языка Python для расширения возможностей функций и классов без изменения их кода.
Есть смартфон. Сделаем его устойчивым к падениям — наденем на него чехол. Чехол не изменяет прежние возможности смартфона, и добавляет к нему качество — ударопрочность. Чехол — декоратор смартфона.
Один и тот же чехол подходит для всех смартфонов нужной модели. Универсальность — важное свойство декораторов.
Анатомия декоратора в Python
Создадим декоратор @hello_decorator
:
Декоратор в Python — функция, которая принимает функцию/класс и возвращает функцию/класс. В примере выше декоратор hello_decorator()
принимает функцию f()
, и возвращает функцию wrapper()
.
Пояснения:
- При объявлении функции
wrapper()
используем встроенный в Python декоратор@wraps
. Этот декоратор копирует свойства__name__
,__doc__
и другие из функцииf()
в функциюwrapper()
, чтобы при отладке программы все выглядело так, будтоwrapper()
и есть функцияf()
; - Аргументы функции
wrapper()
:*args
и**kwargs
. Аргумент*args
собирает позиционные аргументы, а**kwargs
— именованные. Например, в вызове:wrapper(1, ‘a’, x=5, y=None)
значениеargs
— кортеж(1, ‘a’)
, аkwargs
— словарь{‘x’: 5, ‘y’: None}
. Если позиционных аргументов при вызове функции нет,args
— пустой, и если нет именованных аргументов, пустой —kwargs
; - В первой строке тела функции
wrapper()
в консоль выводится «Hello from decorator!» — единственный «побочный эффект» декоратора. Далее вызывается декорируемая функцияf()
; - Функция
f()
внутриwrapper()
принимает параметры*args
и**kwargs
. Операторы*
и**
перед именами параметров в вызове функции имеют противоположный эффект случаю, когда их используют при объявлении аргументов. Например, еслиargs=(1, ‘a’)
иkwargs={‘x’: 5, ‘y’: None}
, вызовf(*args, **kwargs)
равносилен:f(1, ‘a’, x=5, y=None)
. Комбинация «звездочных» аргументов и «звездочных» операторов позволяет универсально передать аргументы из функцииwrapper()
вf()
; - В последней строке функции
hello_decorator()
возвращаем функцию-оберткуwrapper()
. Так мы указываем, что нужно подставить на место декорируемой функцииf()
. Вызывать функциюwrapper()
не нужно — возвращаем саму функцию.
Применим декоратор @hello_decorator
к функции sum2()
:
Функция sum2()
принимает два аргумента и возвращает их сумму. Декорированный sum2()
дополнительно выводит на консоль «Hello from decorator!».
Синтаксис @hello_decorator
введен в Python для удобства, и равносилен такой записи:
Настраиваемый логгер-декоратор
Наметим логирующий декоратор @Logger
для функций с учетом следующих пожеланий:
- Логи отправляются на консоль;
- Вложенность: если логируемая функция вызывает другую логируемую функцию, делаем при выводе на консоль отступ для последней;
- Уровни логов
DEBUG
,INFO
,CRIT
: при декорировании можно указать уровень лога функции, который работает в комбинации с настройкой детальности отображения логов. Если уровень лога функции выше или равен текущей детальности, отображаем лог, иначе — игнорируем; - Для логирования вызова декорированной функции используем шаблон:
LOG_LEVEL [TIMESTAMP] FUNC_NAME(ARGS, KWARGS)
; - Логирование исключений: если функция выбрасывает исключение, логируем его с уровнем
CRIT
; - Внутренние логи: логгер должен работать и в режиме декоратора, и как функция для вывода в лог произвольных сообщений.
Начнем с примера использования. Так мы не перегружаем внимание внутренней сложностью и повышаем шансы создать удачный интерфейс модуля. На этом принципе основана разработка через тестирование — test-driven development (TTD).
Что хотим получить:
После вызова main()
хотим увидеть в консоли:
Наблюдения по поводу декоратора @Logger
:
Logger
— не функция, а класс. Экземпляры этого класса можно вызвать. Результат вызова — декоратор;- У класса
Logger
есть вложенный класс-перечислениеLogLevel
с полямиDEBUG
,INFO
,CRIT
; - Детальность логирования
verbosity
определяется в конструкторе классаLogger
, уровень логирования функцииlog_level
задается при ее декорировании;
У класса Logger
есть метод log_msg()
, который можно использовать напрямую внутри функций.
Напишем скелет класса Logger
:
Конструктор класса Logger
:
Метод Logger.log_msg()
:
Вспомогательный метод Logger.log_func()
:
Ключевой метод-декоратор Logger.__call__()
:
Выводы
- Проектировать проще сверху-вниз: сначала решите, как хотите пользоваться модулем, а потом реализуйте его;
- Если декоратор требует централизованного контроля, используйте класс;
- Если декоратору нужны дополнительные параметры, используйте замыкание — оберните функцию-декоратор во внешнюю функцию (см.
Logger.__call__()
); - Экземпляры классов можно использовать как функции за счет
__call__()
; - Менеджеры контекста удобны, когда нужно «подчищать» состояние.
7К открытий8К показов