← все задачи
Async Python · задача 4 из 10
Частичный успех: что делать с ошибками в gather
Средний
15 минут
return_exceptionsTaskGroupобработка ошибок
Условие
Есть список задач-запросов. Верните два списка: успешные результаты и ошибки с указанием, на каком входе они произошли. Одна неудача не должна отменять остальные.
Что требуется
- Одна упавшая задача не отменяет другие
- По ошибке видно, к какому входному элементу она относится
- Порядок результатов соответствует порядку входа
Пример
ok, failed = await fetch_all(urls)
# ok -> [{"id": 1}, {"id": 3}]
# failed -> [("https://.../2", ClientResponseError(500))]
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Нужно ли повторять упавшие запросы или достаточно вернуть их списком?
- Есть ли порог, после которого разумно прекратить: например, половина запросов упала — внешний сервис лежит
- Отличать ли ошибки сети от ошибок данных — реакция на них разная
Показать решение Скрыть решение
Решение
import asyncio
async def fetch_all(urls, fetch):
results = await asyncio.gather(
*(fetch(url) for url in urls),
return_exceptions=True,
)
succeeded, failed = [], []
for url, result in zip(urls, results):
if isinstance(result, Exception):
failed.append((url, result))
else:
succeeded.append(result)
return succeeded, failed
# Python 3.11+, когда нужно наоборот: первая ошибка отменяет остальные
async def fetch_all_or_fail(urls, fetch):
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(fetch(url)) for url in urls]
return [task.result() for task in tasks]
Почему так
Почему zip с исходным списком
- gather возвращает результаты в порядке аргументов, поэтому позиция однозначно связывает ошибку с входом
- Без этой связи «ClientError» в логе бесполезен: непонятно, какой именно URL сломался
- Тот же приём работает для любых входов, не только URL: zip(inputs, results) — общий шаблон
Почему isinstance(result, Exception), а не BaseException
- CancelledError наследуется от BaseException: если проверять по BaseException, отмена всей операции превратится в «неудачный элемент»
- Отмена — это не ошибка запроса, а сигнал «всё сворачиваем», и её нужно пропускать наверх
- Такая мелочь на собеседовании ценится: видно, что вы понимаете иерархию исключений, а не копируете шаблон
gather против TaskGroup
- gather с return_exceptions — про частичный успех: «сделай что можешь, остальное покажи»
- TaskGroup — про всё или ничего: первая ошибка отменяет остальные задачи и наверх летит ExceptionGroup
- Выбор зависит от задачи: для сбора данных из десятка источников уместен первый, для распределённой транзакции — второй
Что спросят дальше
- Спросят про ExceptionGroup и except* — новый синтаксис 3.11 для разбора групп исключений
- Спросят, что делать с частичным успехом дальше: повторить только упавшие, и тут всплывает retry с backoff
- Могут спросить про «оборванные» задачи: если вы вышли по первой ошибке без TaskGroup, остальные продолжат выполняться — их нужно отменить руками
Следующая задача
Очередь: производитель и потребители — Классическая схема обработки потока: проверяют обратное давление и корректное завершение.