Ваш браузер устарел. Рекомендуем обновить его до последней версии.

Info Board


Файл пакета __init__.py >>

Переменная __all__ >>

if __name__ == '__main__' >>

Вложенные декораторы >>

Декоратор для отладки кода >>

Декораторы в Python

Опубликовано 04.10.2023

Декораторы - это особый вид функций в 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.

И наконец также в папке tests cоздадим файл для теста: test_decorator.py, в котором пропишем декорируемую функцию (test_get_api), где на выходе мы укажем 'return status' для получения ответа, которому в декораторе будет присвоена переменная 'actual_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()
В данном случае конструкция if __name__ нужна для запуска теста через Run.

Запустив (Run) нашу тестовую функцию: test_get_api(), в консоли получим похожий ответ:

Status: 200

Response: {'key': '45x6457…….c9a1c'}

Тест пройден успешно!

Время выполнения теста 'test_get_api': 0.13 сек.


К сведению про __init__.py.

Если в тест добавить функцию, которая импортирует запрос к API из другой папки, то может выйти ошибка: ERROR: not found: ...

Задачка решается добавлением пустого файла с именем: __init__.py в папку с тестами. 
Файлы __init__.py необходимы, для того, чтобы Python рассматривал текущую директорию, как пакет. Это делается для того, чтобы предотвратить директории с общим именем, например string, от непреднамеренного скрытия допустимых модулей, которые происходят позже на пути поиска модуля. Пакет в Python – это набор из нескольких файлов модулей в одной директории/папке.

Можно оставить __init__.py пустым, в таком случае он будет выполнять код инициализации для пакета или установить переменную __all__.

Проще говоря, переменная __all__ даёт некоторый контроль над импортом того, что мы хотим импортировать, когда указана конструкция import * (импортировать всё). В этом случае импортируются не все объекты, а только те, которые прописаны в файле __init__.py в переменной __all__. Например: __all__ = ["MyClass", "MyClass2"]