Декораторы - это особый вид функций в Python, которые позволяют изменять работу или поведение других функций без изменения их исходного кода. Декораторы предназначены для подключения любого дополнительного поведения к основной функции, называемой декорируемой, которое может выполняться до, после или даже вместо основной функции...
Декораторы в Python чаще всего используются для модификации или расширения функциональности существующих функций или классов. Они могут использоваться для:
1. Расширения функциональности. Например, для логирования выполнения функций, замера времени работы функций, проверки дополнительных условий, разрешающих выполнение указанной функции и прочее...
2. Проверки пользовательской аутентификации или авторизации. Например, декоратор может проверять наличие аутентификационного токена или прав доступа пользователя перед выполнением операции.
3. Проверки и валидации входных параметров. Например, декоратор может проверять, что переданные аргументы функции соответствуют ожидаемым типам данных или значениям.
4. Кэширования результатов выполнения функций или методов классов. Например, декоратор может сохранять результат выполнения функции в памяти или в базе данных и возвращать сохранённое значение при повторных вызовах функции с теми же входными параметрами.
5. Оптимизации или профилирования. Например, декораторы могут добавлять логику для сбора статистики использования функций или методов, или для применения оптимизации, такой как динамическая компиляция функции.
Декорируемая функция - это функция с основным рабочим кодом, к которой применяется декоратор. В этом блоге предлагаю рассмотреть пример простого декоратора, который будет запускать код декорируемой функции заданное количество раз (кстати удобно для нагрузочного тестирования).
Итак, сам мистер декоратор:
Создаём функцию/декоратор 'do_it_much' для какой-то декорируемой функции, которую обозначим, как: 'func':
def do_it_much(func):
n = 3 # установим кол-во запусков
def wrapper(*args, **kwargs):
for i in range(n):
func(*args, **kwargs) # обращение к декорируемой функции
return wrapper
Функция wrapper, внутри декоратора, выступает, как обёртка/шаблон для декорирования рабочей функции. [*args, **kwargs] – аргументы, которые могут быть в рабочей функции... В тело функции wrapper добавляем код с нужным нам функционалом – т.е. какое-то дополнение, которое будет применимо к работе декорируемой функции, при этом оно не изменит работу кода внутри самой функции.
В нашем случае мы хотим, чтобы декорируемая функция запускалась n-раз подряд. Теперь добавим наш декоратор перед функцией, которую нужно декорировать (назовём её number) с помощью метода, который носит название синтаксического сахара: @do_it_much.
Синтаксический сахар – это синтаксические возможности, применение которых не влияет на поведение программы, но делает её использование более удобным. Синтаксический сахар в декораторах устанавливается путём добавления символа: '@' к названию функции-декоратора, что позволяет не указывать больше никаких аргументов. Код декорируемой функции будет выглядеть так:
@do_it_much # добавляем наш декоратор перед функцией
def number(x, y): # декорируемая функция
print(x ** y)
number(3, 4)
В данном варианте, декорируемая функция выполняет операцию возведения числа Х в степень числа Y. Применение нашего декоратора запустит выполнение кода декорируемой функции 3 раза. Т.е. на выходе мы получим: три цифры 81.
По-хорошему, декораторы, как и фикстуры (о них позже), питонистично располагать в отдельном специальном файле: conftest.py (он создаётся вручную). При этом сами декораторы нужно импортировать в файл, где расположена декорируемая функция, по такому шаблону:
from conftest import do_it_much
Давайте разберём ещё один пример, а заодно напишем тестовую функцию.
Предварительно, нужно пройти регистрацию на сайте: https://petfriends.skillfactory.ru. Создадим в IDE (у меня это PyCharm) новый проект, в который добавим две папки: First и tests. В первой папке создадим файлы API.py и Settings.py (в него добавляем переменные: valid_email и valid_password, которые содержат валидные email и пароль для входа на сайт). В файле API создаём GET-запрос для получения ключа пользователя:
import requests import json
def get_api_key(email: str, password: str) -> json:
"""Метод делает запрос к API сервера и возвращает статус запроса и ответ
в формате JSON или text с уникальным ключом пользователя, найденного
по email и паролю"""
url = "https://petfriends.skillfactory.ru/" # Укажем заголовки запроса для получения ключа пользователя: headers = {'email': email, 'password': password} # Отправляем GET запрос: res = requests.get(url + 'api/key', headers=headers) # Получаем статус-КОД ответа: status = res.status_code # Если ответ мы получим не в формате json, возьмём его как текст: try: result = res.json() except json.decoder.JSONDecodeError: result = res.text return status, result
Затем в папке tests создаём пустой файл с именем: '__init__.py'. Он нужен для инициализации нашей папки tests, как пакета (см. пояснения ниже). А также создаём файл conftest.py, в котором напишем следующую функцию-декоратор:
import time
def api_test(expected_status): def decorator(func): def wrapper(*args, **kwargs): # Засечём текущее время для отслеживания времени выполнения
# декорируемой функции: start_time = time.perf_counter() # Результат вывода декорируемой функции определим в переменную: actual_status = func(*args, **kwargs) # Проверяем статус-код ответа if actual_status != expected_status: print(f"Ошибка: ожидался статус код {expected_status}, \
получен {actual_status}") else: print("Тест пройден успешно!") # Засекаем время по завершению теста: end_time = time.perf_counter() print(f"Время выполнения теста '{func.__name__}': \
{(end_time - start_time):.02f} сек.") return actual_status return wrapper return decorator
Можно заметить, что декоратор помещён в функцию api_test(). Это сделано для того, чтобы декоратор исполнялся в зависимости от значения в аргументе функции: expected_status.
from tests.conftest import api_test from First.Settings import valid_email, valid_password from First.API import get_api_key
@api_test(expected_status=200) # Наш декоратор. Ожидаем получить статус-Код = 200 def test_get_api(email=valid_email, password=valid_password):
"""Проверяем что запрос api ключа возвращает статус 200 и в ответе содержится слово key"""
# Отправляем запрос: status, result = get_api_key(email, password) # И выводим полученный ответ в консоль: print('\nStatus:', status) print('Response:', result)
# Сверяем полученный ответ с нашими ожиданиями: assert status == 200 assert 'key' in result return status def main(): test_get_api()
if __name__ == '__main__': main()
Запустив (Run) нашу тестовую функцию: test_get_api(), в консоли получим похожий ответ:
Status: 200
Response: {'key': '45x6457…….c9a1c'}
Тест пройден успешно!
Время выполнения теста 'test_get_api': 0.13 сек.
Если в тест добавить функцию, которая импортирует запрос к API из другой папки, то может выйти ошибка: ERROR: not found: ...
Задачка решается добавлением пустого файла с именем: __init__.py в папку с тестами.
Файлы __init__.py необходимы, для того, чтобы Python рассматривал текущую директорию, как пакет. Это делается для того, чтобы предотвратить директории с общим именем, например string, от непреднамеренного скрытия допустимых модулей, которые происходят позже на пути поиска модуля. Пакет в Python – это набор из нескольких файлов модулей в одной директории/папке.
Можно оставить __init__.py пустым, в таком случае он будет выполнять код инициализации для пакета или установить переменную __all__.
Проще говоря, переменная __all__ даёт некоторый контроль над импортом того, что мы хотим импортировать, когда указана конструкция import * (импортировать всё). В этом случае импортируются не все объекты, а только те, которые прописаны в файле __init__.py в переменной __all__. Например: __all__ = ["MyClass", "MyClass2"]
