Как создать собственные Python-декораторы и правильно их использовать
Статья рассчитана на тех, кто владеет основами Python и хочет научиться создавать собственные декораторы для повышения качества кода.
8К открытий10К показов
Статья рассчитана на тех, кто владеет основами 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__(); - Менеджеры контекста удобны, когда нужно «подчищать» состояние.
8К открытий10К показов



