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

Iuliia Averianova
NOP::Nuances of Programming
4 min readDec 6, 2020

--

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 возвращает эту функцию:
def new_func(x):
return first_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 и Яндекс.Дзен

--

--