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
:
Заворачиваем функцию. Не переживайте, всё не так уж сложно. Пройдём по коду шаг за шагом:
- Самая первая функция
retry
параметризует декоратор, то есть указывает, какие исключения мы хотим обработать, частоту попыток, интервал ожидания между попытками и каков экспоненциальный фактор возврата — число, на которое умножается время ожидания каждый раз при неудачной попытке. retry_decorator
: это параметризованный декоратор, который возвращается функциейretry
. Мы декорируем функцию вretry_decorator
с помощью@wraps
. Строго говоря, это не так уж необходимо, когда речь идёт о функциональности. Эта функция-обёртка обновляет__name__
и__doc__
обёрнутой функции: если этого не сделать, функция__name__
всегда будетfunc_with_retries
).func_with_retries
применяет логику повтора. Эта функция оборачивает вызовы в блокиtry-except
и реализует экспоненциальное ожидание возврата с некоторым логированием.
Применение
Как альтернатива, немного более конкретно:
Результаты:
Вызов декорированной функции и столкновение с ошибками приведёт к следующему:
Вызываемая функция дважды завершилась с ошибкой ConnectionRefusedError
, один раз с ConnectionResetError
и успешно выполнилась с четвёртой попытки.
Здесь у нас есть информативное логирование, мы отображаем args
и kwargs
и имя функции, что облегчает отладку и исправление ошибок в случае, когда ошибка не устраняется после всех попыток.
Заключение
Мы разобрали, как применять декораторы в Python и как декорировать критически важные функции декоратором retry
, чтобы они выполнялись даже в условиях неопределённости.
Читайте также:
- Когда и зачем использовать оператор := в Python
- Знакомство с функциональным программированием в Python, JavaScript и Java
- 5 ключевых понятий Python и их магические методы
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Fabian Bosler: Are you using Python with APIs? Learn how to use a retry decorator!