DRY vs Дзен Python в задаче о троттлинге в Amazon-отчетах
Скажу, что вопрос носит общий характер, не требует знания сервисов Amazon. Нужно "только" ощущение красивого в Puthon ))
Всем известен принцип DRY - не повторяй себя. Он не вошел в Дзен Python в явном виде, возможно из ощущения "общепринятости". У меня практический вопрос, плюс некоторые рассуждения. Код не будет повторяемым, меня интересует ответ - конструкция. Как бы вы сделали. Итак, начнем.
У меня есть задача про получение отчетов Amazon через API. Хотя у любого подобного сервиса продаж через сайт есть понятие троттлинг, примененное к получению отчетов. Скажем 15 подряд, а затем 1 в минуту. Если вы сделаете 16 кряду в цикле, вылетит исключение и надо подождать. Вот код предлагаемого решения
infinity = True
while infinity:
try:
request = x.request_report(amz_report_name,
start_date=bytes(auto_report.reportstart, CODING),
end_date=bytes(auto_report.reportend, CODING),
)
infinity = False
except Exception as e:
if TROTTLING_MESSAGE in e:
time.sleep(61)
else:
assert False, f"Unknown error: \n {e}"
Важно указать, что типов запросов с полтора десятка и у них могут быть разный набор как ключевых так и позиционных параметров.
То есть везде в коде, где мне нужно получить от Amazon отчет, я должен "обвязать" непосредственное обращение к методу конструкцией try-except одного и того же вида.
Одним из напрашивающихся решений в таких случаях - механизм декораторов. Ну запихиваем нашу "обвязку" в декоратор, и декорируем необходимый вызов. Однако я вызываю не свои функции а методы модуля mws, поэтому для того чтобы применить декоратор, мне нужно для каждого такого метода нужно сделать обертку - функцию, если я не ошибаюсь. Плюс - разные параметры потребуют использовать *args, **kwargs в декораторе.
Итого, я избавляюсь от повторения себя, получаю удобно менять try-except в одном месте, если вдруг понадобится, но при этом придется наплодить оберток методов, оберток на оберток и нарушить три принципа Zen Python:
Explicit is better than implicit.
Simple is better than complex.
...
Readability counts.
Итого вопрос. Какая конструкция все же лучше - набросать везде понятных и читаемых try-except или снизить читаемость и "навертеть" декораторов? Особенно буду признателен, если поделетись "шаблоном для подобного случая" из своего опыта.
Ну и да - получается, что это философский вопрос - что важнее DRY или Zen Python
Что касается меня, то возможно я боюсь сложной отладки связанной с декораторами, которой я еще никогда не делал...
Ответы (2 шт):
Можно написать функцию обертку (wrapper function):
def try_or_sleep(func, retry_count=30, retry_sleep=61, *args, **kwargs):
for _ in range(retry_count):
try:
return func(*args, **kwargs)
except Exception as e:
if TROTTLING_MESSAGE in e:
time.sleep(retry_sleep)
else:
assert False, f"Unknown error: \n {e}"
return None
Пример вызова:
request = try_or_sleep(
func=x.request_report,
retry_count=30,
retry_sleep=61,
args=(amz_report_name,),
start_date=bytes(auto_report.reportstart, CODING),
end_date=bytes(auto_report.reportend, CODING))
Вот к какому решению я пришел, основываясь на идее ответа @MaxU.
Мне этот "шаблон" кажется важным и новым, хотя возможно не самым распространенным.
Кратко о проблеме: возникла необходимость декорировать МЕТОД класса из чужого модуля. Можно было бы сделать обертку на метод, превратив его в свою функцию, однако мне показалось это громоздким. Предлагаемое решение - функция декоратор без декорирования функции при помощи оператора @. А прямо в коде явно вызываем декоратор с нужным методом.
Это соблюдает принцип DRY и Дзен Python.
def try_or_sleep(func, sleep_secs=60):
"""
Обертка на метод-запрос для учета троттлинга.
Parameters
----------
func : метод-запрос к Amazon модкля mws.
sleep_secs : количество секунд до возможности обработки сервером нового запрос
Returns
-------
объект mws.request - структурированный ответ сервера Amazon.
"""
print('> декоратор с аргументами...')
def decorated(*args, **kwargs):
print('до вызова функции', func.__name__)
infinity = True
while infinity:
try:
request = func(*args, **kwargs)
break
except Exception as e:
if TROTTLING_MESSAGE in str(e):
print(f"{40*'='} awaiting {sleep_secs} secs")
time.sleep(sleep_secs + 1)
else:
assert False, f"Unknown error: \n {e}"
print('после вызова функции', func.__name__)
return request
return decorated
И вызов выглядит теперь следующим образом:
request = try_or_sleep(x.request_report)(
amz_report_name,
start_date=bytes(auto_report.reportstart, CODING),
end_date=bytes(auto_report.reportend, CODING),
)
Теперь, глядя на код, вы видите, что метод x.request_report оборачивается в try_or_sleep(...), а его (метода) параметры остались на месте. Как сказано в Дзен Python
Explicit is better than implicit.
Simple is better than complex.
...
Readability counts.
И - да. Простите код с комментариями и отладочной печатью...