Декораторы: Изменение поведения функции — Часть 2

Изменение поведения функции

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

Содержание статьи

Ниже представлен список статей данного курса о декораторах в Python:

Обзор простых декораторов в Python

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

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

Обратите внимание, как текст decorating ... появляется при определении функции, а не при ее вызове.

Причина этого в том, что Python вызывает функцию декоратора во время объявления декорированной функции.

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

Декораторы, которые заменяют декорированные функции

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

Начнем с изменения оператора return, чтобы он возвращал функцию, отличную от f. Вернемся к оболочке Python:

Здесь была определена функция forty_two(), и затем декоратор возвращал ссылку на данную функцию вместо f, которая была использована выше. Давайте используем данный декоратор, как было сделано выше, чтобы понять, чего именно мы добились данным изменением:

Результат:

Что случилось? Каким-то образом оказалось, что декорированная функция повреждена и не может быть вызвана, как было сделано ранее.

Напомним, что функция, возвращаемая декоратором, заменяет исходную функцию. Декоратор по сути изменяет функцию my_function, чтобы она была ссылкой на forty_two, поэтому при вызове my_function(1, 2) была получена ошибка. В действительности произошел вызов forty_two(1, 2), что недопустимо, поскольку эта функция не принимает аргументов.

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

Как устранить ошибку? Поскольку my_function теперь является названием для forty_two, нам нужно вызвать эту функцию без аргументов:

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

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

В такой момент становится актуальным понятие внутренних функций. Взгляните на следующий декоратор:

Здесь функция wrapped() является внутренней функцией, потому что она определена внутри тела другой функции.

Важной особенностью внутренних функций является тот факт, что они они переносят с собой любые переменные в области видимости родительской функции, который в программировании называется замыканием. Обратите внимание, как внутри тела функции wrapped() можно вызвать f(1, 2), хотя f не определена внутри функции и вместо этого становится аргументом родительской функции my_decorator().

Проверим новый декоратор:

Результат:

Интересно, правда? Мы по-прежнему не можем передать аргументы a и b оригинальной функции my_function(), но при ее вызове без аргументов мы получаем в результате 3, потому что функция wrapped() внедряет аргументы 1 и 2.

Чтобы полностью восстановить два аргумента из исходной функции, можно определить функцию wrapped() также с двумя аргументами и передать эти аргументы в f:

Конечно, данный декоратор можно применить к функциям, у которых по два аргумента. Чтобы функция wrapped() работала в качестве обертки для всех функций, нужно написать ее код таким образом, чтобы она принимала любые аргументы и затем передавала их f.

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

Наконец это «ничего не делающая» функция, которая совместима со всеми функциями вне зависимости от их аргументов, имплементированна во внутренней функцией.

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

Посмотрите комментарии к следующему примеру, которые показывают, где в декораторе можно вставить эти новые варианты поведения:

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

Результат:

Внедрение новых аргументов для функции через декоратор

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

В качестве примера рассмотрим следующий декоратор, который вставляет текущее время в качестве первого аргумента в любые функции, которую он декорирует:

Далее дан пример использования:

Результат:

Как видите, декорированная функция написана для принятия первого аргумента time, но данный аргумент автоматически добавляется декоратором, так что функция вызывается с оставшимися аргументами, в данном случае с a и b.

Меняем результат функции через декоратор

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

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

Хороший пример этой техники можно применить к веб-фреймворку Flask. Рассмотрим следующую функцию представления Flask:

Если приложение на Flask имплементирует API, скорее всего у вас есть много путей, которые заканчиваются возвращением результата в формате JSON, сгенерированного с помощью функции jsonify(). Было бы неплохо, если бы была возможность возвращения словаря вместо необходимости использования функции jsonify() в конце каждой функции?

Декоратор to_json может сделать конвертацию в JSON формате за вас:

Здесь функция index() возвращает словарь, который во Flask является недействительным типом для ответа. Однако декоратор to_json оборачивает функцию, и возвращает ответ в JSON. Далее дан полный код приложения, включая имплементацию данного декоратора:

Функция wrapped() просто вызывает оригинальную функцию, в данном контексте она называется f, и затем проверяет, если возвращаемое значение является словарем или списком. Если так, то вызывается функция jsonify(), эффективно интерпретируя и исправляя возвращаемое значение перед его возвращением во фреймворк.

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

Проверка данных при помощи декораторов

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

Далее представлен пример этой техники для Flask:

В данном примере, декоратор only_admins ищет HTTP заголовок X-Auth-Token во входящем запросе и затем проверяет, если он совпадает с секретным токеном администратора, который для простоты мы сделали константой. Если нет заголовка токена, или если он есть, но не совпадает, то функция abort() из Flask выполняется для генерации ответа 401 и остановки дальнейших запросов. В противном случае запрос может пройти, вызвав при этом декорированную функцию.

Обратите внимание, как в примере функции представления admin_route() используются декораторы app.route и only_admins. Это называется цепью декораторов. Цепь декораторов является сложной темой, о которой мы поговорим в будущих статьях. При работе с Flask вы должны понимать, что почти всегда декоратор app.route будет первым декоратором в цепи.

Заключение

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