Добро пожаловать на второй урок курса о декораторах в Python. В первой статье был показан самый простой стиль декораторов, которые используются для регистрации функций в качестве обработчиков или обратных вызовов для событий. В данной части будут представлены более интересные декораторы, которые изменяют или дополняют поведение декорированной функции.
Содержание статьи
- Обзор простых декораторов в Python
- Декораторы, которые заменяют декорированные функции
- Внедрение новых аргументов для функции через декоратор
- Меняем результат функции через декоратор
- Проверка данных при помощи декораторов
Ниже представлен список статей данного курса о декораторах в Python:
- Часть 1: Регистрация функции;
- Часть 2: Изменение поведения функции (данная статья);
- Часть 3: Декораторы с аргументами.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Обзор простых декораторов в Python
Прежде чем мы углубимся в новую территорию, давайте рассмотрим, как работают простые декораторы из первого урока. Ниже представлен пример, введенный нами в оболочку IDLE Python. Попробуйте поэкспериментировать, запустите IDLE оболочку и введите код сами.
1 2 3 |
def my_decorator(f): print('decorating', f) return f |
Данный пример декоратора вполне допустим, но кроме вывода сообщения в терминале он ничего не делает. При желании использовать такой декоратор для функции можно сделать что-то вроде этого:
1 2 3 4 5 6 7 8 |
>>> @my_decorator ... def my_function(a, b): ... return a + b ... decorating <function my_function at 0x10ae241e0> >>> my_function(1, 2) 3 >>> |
Обратите внимание, как текст decorating ...
появляется при определении функции, а не при ее вызове.
Причина этого в том, что Python вызывает функцию декоратора во время объявления декорированной функции.
Этот декоратор ничего не делает, поэтому функция my_function()
не меняется и может вызываться обычным способом.
Декораторы, которые заменяют декорированные функции
Более продвинутые декораторы, которые представлены далее, не будут возвращать ту же функцию, что и приведена выше. Они будут возвращать другую функцию, и это позволит реализовать множество очень интересных трюков, с которыми более простые декораторы не справятся. Почти всегда такие декораторы реализуются с помощью внутренних функций, что для многих разработчиков является странной и непонятной особенностью языка Python. Для более понятного представления темы мы можем преобразовать вышеуказанный «ничего не делающий» декоратор в более мощный инструмент. Это будет сделано поэтапно.
Начнем с изменения оператора return
, чтобы он возвращал функцию, отличную от f
. Вернемся к оболочке Python:
1 2 3 4 5 6 |
def forty_two(): return 42 def my_decorator(f): print('decorating', f) return forty_two |
Здесь была определена функция forty_two()
, и затем декоратор возвращал ссылку на данную функцию вместо f
, которая была использована выше. Давайте используем данный декоратор, как было сделано выше, чтобы понять, чего именно мы добились данным изменением:
1 2 3 |
@my_decorator def my_function(a, b): return a + b |
Результат:
1 2 3 4 5 6 |
decorating <function my_function at 0x10ae24268> >>> my_function(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: forty_two() takes 0 positional arguments but 2 were given >>> |
Что случилось? Каким-то образом оказалось, что декорированная функция повреждена и не может быть вызвана, как было сделано ранее.
Напомним, что функция, возвращаемая декоратором, заменяет исходную функцию. Декоратор по сути изменяет функцию my_function
, чтобы она была ссылкой на forty_two
, поэтому при вызове my_function(1, 2)
была получена ошибка. В действительности произошел вызов forty_two(1, 2)
, что недопустимо, поскольку эта функция не принимает аргументов.
Обратите внимание, сообщении об ошибке называет функцию forty_two
, хотя мы определили ее с названием my_function
. Данный побочный эффект именования декораторов, возвращающих другую функцию, может сбивать с толку. У этой проблемы есть решение, но это тема для будущей статьи данного курса.
Как устранить ошибку? Поскольку my_function
теперь является названием для forty_two
, нам нужно вызвать эту функцию без аргументов:
1 2 |
>>> my_function() 42 |
Вам может быть интересно, где сейчас исходная функция. В этом примере, к сожалению, исходной функции больше нет, поскольку декоратор заменил ее на функцию forty_two()
.
Очевидно, что это ужасный декоратор, который совершенно бесполезен. Идея состоит в том, что возвращаемая функция должна действовать как оболочка для исходной функции, а не как полная её замена.
В такой момент становится актуальным понятие внутренних функций. Взгляните на следующий декоратор:
1 2 3 4 5 |
def my_decorator(f): def wrapped(): return f(1, 2) print('decorating', f) return wrapped |
Здесь функция wrapped()
является внутренней функцией, потому что она определена внутри тела другой функции.
Важной особенностью внутренних функций является тот факт, что они они переносят с собой любые переменные в области видимости родительской функции, который в программировании называется замыканием. Обратите внимание, как внутри тела функции wrapped()
можно вызвать f(1, 2)
, хотя f
не определена внутри функции и вместо этого становится аргументом родительской функции my_decorator()
.
Проверим новый декоратор:
1 2 3 |
@my_decorator def my_function(a, b): return a + b |
Результат:
1 2 3 4 5 6 7 8 9 |
decorating <function my_function at 0x10ae24378> >>> my_function(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: wrapped() takes 0 positional arguments but 2 were given >>> my_function() 3 |
Интересно, правда? Мы по-прежнему не можем передать аргументы a
и b
оригинальной функции my_function()
, но при ее вызове без аргументов мы получаем в результате 3
, потому что функция wrapped()
внедряет аргументы 1
и 2
.
Чтобы полностью восстановить два аргумента из исходной функции, можно определить функцию wrapped()
также с двумя аргументами и передать эти аргументы в f
:
1 2 3 4 5 |
def my_decorator(f): def wrapped(a, b): return f(a, b) print('decorating', f) return wrapped |
Конечно, данный декоратор можно применить к функциям, у которых по два аргумента. Чтобы функция wrapped()
работала в качестве обертки для всех функций, нужно написать ее код таким образом, чтобы она принимала любые аргументы и затем передавала их f
.
В Python можно создать функцию для принятия любых аргументов со специальными аргументами *args и **kwargs, которые представляют позиционные аргументы и аргументы ключевых слов соответственно.
1 2 3 4 5 |
def my_decorator(f): def wrapped(*args, **kwargs): return f(*args, **kwargs) print('decorating', f) return wrapped |
Наконец это «ничего не делающая» функция, которая совместима со всеми функциями вне зависимости от их аргументов, имплементированна во внутренней функцией.
Декоратор, структурированный подобным образом, может вставить дополнительное поведение, которое расширяет возможности функции либо перед или после вызова данной функции внутри функции wrapped()
. Также можно решить не вызывать декорированную функцию в определенных случаях, например, если она определяет, что переданные аргументы неверны.
Посмотрите комментарии к следующему примеру, которые показывают, где в декораторе можно вставить эти новые варианты поведения:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def my_decorator(f): def wrapped(*args, **kwargs): # ... # вставляется код, который запускается перед декорированной функцией # (и опционально можно не вызывать данную функцию) # ... response = f(*args, **kwargs) # ... # вставляется код, который запускается после декорированной функцией # (и опционально решается, менять ли ответ) # ... return response return wrapped |
Чтобы помочь визуализировать, как данный стиль декоратора работает, далее дан пример использования операторов вывода print
в важных фрагментах кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def my_decorator(f): def wrapped(*args, **kwargs): print('До функции') response = f(*args, **kwargs) print('После функции') return response print('декорируем', f) return wrapped @my_decorator def my_function(a, b): print('В функции') return a + b |
Результат:
1 2 3 4 5 |
декорируем <function my_function at 0x10ae24268> >>> my_function(1, 2) До функции В функции После функции |
Внедрение новых аргументов для функции через декоратор
Очевидный трюк, который можно реализовать с помощью такого типа декораторов, на который был дан намек выше — изменить список аргументов, который отправляется декорированной функции при ее вызове.
В качестве примера рассмотрим следующий декоратор, который вставляет текущее время в качестве первого аргумента в любые функции, которую он декорирует:
1 2 3 4 5 6 7 |
from datetime import datetime def add_current_time(f): def wrapped(*args, **kwargs): return f(datetime.utcnow(), *args, **kwargs) return wrapped |
Далее дан пример использования:
1 2 3 |
@add_current_time def test(time, a, b): print('Я получил аргументы', a, b, 'at', time) |
Результат:
1 2 3 |
>>> test(1, 2) Я получил аргументы 1 2 at 2019-10-10 21:38:35.582887 |
Как видите, декорированная функция написана для принятия первого аргумента time
, но данный аргумент автоматически добавляется декоратором, так что функция вызывается с оставшимися аргументами, в данном случае с a
и b
.
Меняем результат функции через декоратор
Еще одна очень распространенная задача, для которой подходят декораторы это изменение возвращаемого значения декорированной функции.
Если у вас есть много функций, которые вызывают функцию конвертации данных перед возвратом, вы можете переместить эту задачу конвертации в декоратор, чтобы сделать код в функциях более простым, менее повторяющимся и более удобным для чтения.
Хороший пример этой техники можно применить к веб-фреймворку Flask. Рассмотрим следующую функцию представления Flask:
1 2 3 |
@app.route('/') def index(): return jsonify({'hello': 'world'}) |
Если приложение на Flask имплементирует API, скорее всего у вас есть много путей, которые заканчиваются возвращением результата в формате JSON, сгенерированного с помощью функции jsonify()
. Было бы неплохо, если бы была возможность возвращения словаря вместо необходимости использования функции jsonify()
в конце каждой функции?
Декоратор to_json
может сделать конвертацию в JSON формате за вас:
1 2 3 4 |
@app.route('/') @to_json def index(): return {'hello': 'world'} |
Здесь функция index()
возвращает словарь, который во Flask является недействительным типом для ответа. Однако декоратор to_json
оборачивает функцию, и возвращает ответ в JSON. Далее дан полный код приложения, включая имплементацию данного декоратора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from flask import Flask, jsonify app = Flask(__name__) def to_json(f): def wrapped(*args, **kwargs): response = f(*args, **kwargs) if isinstance(response, (dict, list)): response = jsonify(response) return response return wrapped @app.route('/') @to_json def index(): return {'hello': 'world'} |
Функция wrapped()
просто вызывает оригинальную функцию, в данном контексте она называется f
, и затем проверяет, если возвращаемое значение является словарем или списком. Если так, то вызывается функция jsonify()
, эффективно интерпретируя и исправляя возвращаемое значение перед его возвращением во фреймворк.
На заметку: В версии Flask 1.1 функция может вернуть словарь, и Flask автоматически конвертирует его в JSON. Вышеуказанный декоратор больше не нужен. Но, это по-прежнему хороший пример создания фильтров, использующих паттерн декоратора.
Проверка данных при помощи декораторов
Еще один полезный метод, который может быть реализован с помощью декораторов, заключается в проверке данных до запуска декорированной функции. Очень распространенный этому пример в веб-приложении — это аутентификация пользователя. Если задача проверки/аутентификации завершается неудачно, то декорированная функция не вызывается, и вместо нее появляется ошибка.
Далее представлен пример этой техники для Flask:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from flask import request, abort ADMIN_TOKEN='fheje3$93m*fe!' def only_admins(f): def wrapped(*args, **kwargs): token = request.headers.get('X-Auth-Token') if token != ADMIN_TOKEN: abort(401) # не авторизован return f(*args, **kwargs) return wrapped @app.route('/admin') @only_admins def admin_route(): return "только администраторы могут получить доступ к этому маршруту!" |
В данном примере, декоратор only_admins
ищет HTTP заголовок X-Auth-Token
во входящем запросе и затем проверяет, если он совпадает с секретным токеном администратора, который для простоты мы сделали константой. Если нет заголовка токена, или если он есть, но не совпадает, то функция abort()
из Flask выполняется для генерации ответа 401 и остановки дальнейших запросов. В противном случае запрос может пройти, вызвав при этом декорированную функцию.
Обратите внимание, как в примере функции представления admin_route()
используются декораторы app.route
и only_admins
. Это называется цепью декораторов. Цепь декораторов является сложной темой, о которой мы поговорим в будущих статьях. При работе с Flask вы должны понимать, что почти всегда декоратор app.route
будет первым декоратором в цепи.
Заключение
Пока мы рассматривали только те декораторы, которые сами не принимают никаких аргументов, аргументы всегда передаются декорированной функции. Это было сделано специально, потому что декораторы, которые принимают собственные аргументы в дополнение к тем, которые передаются в декорированную функцию, создавать сложнее. Это будет темой следующих статей, поэтому ознакомьтесь с концепциями, которые были рассмотрены в этой и предыдущей статьях, и будьте готовы к еще новому уровню сложности в следующей части.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»