Python: декоратор @retry

Python часто называют “склеивающим” языком. Для меня этот термин означает, что язык помогает соединять системы и обеспечивает передачу данных из A в B в желаемой структуре и формате.

Я создал бесчисленное количество ETL-скриптов  —  Extraction Transformation Load — извлечение, преобразование, загрузка на Python. Все эти сценарии работают по сути по одному и тому же принципу: откуда-то извлекают данные, преобразуют их и затем выполняют последнюю операцию. Последней операцией обычно бывает загрузка данных куда-либо, но также может быть условное удаление.

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

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

Я обнаружил, что очень простой декоратор retry может стать спасением в подобных ситуациях. Большинство моих проектов в тот или иной момент в итоге содержат декоратор retry в каком-либо утилитном модуле.

Декоратор

Функции — это объекты первого уровня

В Python функции являются объектами первого уровня. То есть функция  —  это тоже объект. Этот факт помимо всего прочего означает, что функцию можно динамически создавать, передавать в саму эту функцию и даже изменять. Взгляните на простейший пример:

def my_function(x):
    print(x)

IN:
my_function(2)
OUT:
2

IN:
my_function.yolo = 'you live only once'
print(my_function.yolo)
OUT:
'you live only once'

Декорирование функции

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

def first_func(x):
    return x**2
    
def second_func(x):
    return x - 2

Обе функции завершатся ошибкой при вызове со строкой '2'. Мы можем добавить функцию преобразования типа и декорировать этой функцией first_func и second_func.

def convert_to_numeric(func):


    # определяем функцию во внешней функции
    def new_func(x):
        return func(float(x))

    # возвращаем вновь определённую функцию
    return new_func

Эта функция-обёртка convert_to_numeric требует другую функцию в качестве аргумента и возвращает другую функцию. Теперь, когда мы оборачиваем функции и вызываем их со строковым числом, всё работает:

IN:
new_fist_func = convert_to_numeric(first_func)

###############################
convert_to_numeric возвращает эту функцию:
defnew_func(x):
    returnfirst_func(float(x))
###############################

new_fist_func('2')
OUT:
4.0

IN:
convert_to_numeric(second_func)('2')
OUT:
0

Что здесь происходит?

Наша convert_to_numeric принимает функцию (A) в качестве аргумента и возвращает новую функцию (B). Новая функция (B) при вызове вызывает функцию (A), но не с переданным аргументомx, а с float(x), и таким образом решает проблему TypeError.

Синтаксис декоратора

Для упрощения работы Python предоставляет специальный синтаксис:

@convert_to_numeric
def first_func(x):
    return x**2

Синтаксис выше эквивалентен этому коду:

def first_func(x):
    return x**2

first_func = convert_to_numeric(first_func)

Развёрнутый синтаксис проясняет, что именно здесь происходит, особенно при применении нескольких декораторов.

Retry!

Теперь, когда мы разобрались в основах, давайте перейдём к моему любимому и широко используемому декоратору retry:

Код здесь!

Заворачиваем функцию. Не переживайте, всё не так уж сложно. Пройдём по коду шаг за шагом:

  1. Самая первая функция retry параметризует декоратор, то есть указывает, какие исключения мы хотим обработать, частоту попыток, интервал ожидания между попытками и каков экспоненциальный фактор возврата  —  число, на которое умножается время ожидания каждый раз при неудачной попытке.
  2. retry_decorator: это параметризованный декоратор, который возвращается функцией retry. Мы декорируем функцию в retry_decoratorс помощью @wraps. Строго говоря, это не так уж необходимо, когда речь идёт о функциональности. Эта функция-обёртка обновляет __name__ и __doc__ обёрнутой функции: если этого не сделать, функция __name__ всегда будет func_with_retries).
  3. func_with_retries применяет логику повтора. Эта функция оборачивает вызовы в блоки try-except и реализует экспоненциальное ожидание возврата с некоторым логированием. 

Применение

Функция, декорированная с помощью retry, предпринимающим четыре попытки до любого исключения

Как альтернатива, немного более конкретно:

Функция, декорированная retry в ответ на TimeoutError предпринимает две попытки 

Результаты:

Вызов декорированной функции и столкновение с ошибками приведёт к следующему:

Вызываемая функция дважды завершилась с ошибкой ConnectionRefusedError, один раз с ConnectionResetError и успешно выполнилась с четвёртой попытки.

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

Заключение

Мы разобрали, как применять декораторы в Python и как декорировать критически важные функции декоратором retry, чтобы они выполнялись даже в условиях неопределённости.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Fabian Bosler: Are you using Python with APIs? Learn how to use a retry decorator!

Предыдущая статьяРазвертывание Gatsby-сайта с помощью GitHub Actions
Следующая статьяСоздание многопользовательской игры с использованием Socket.io при помощи NodeJS и React