Рассмотрите async for: какое нарушение протокола возникает, если __aiter__ возвращает корутину вместо асинхронного итератора?
В современном Python метод __aiter__ должен сразу вернуть асинхронный итератор — объект с методом __anext__. Если он возвращает корутину, async for не ожидает её автоматически и завершается ошибкой протокола, обычно TypeError.
Асинхронная итерация появилась для последовательного получения данных, когда очередной элемент может потребовать ожидания: например, при чтении из сети или асинхронного потока. Протокол разделяет получение итератора и получение следующего элемента, чтобы сам __aiter__ не был лишней точкой ожидания.
В ранних версиях поддержки асинхронных итераторов допускался вариант, при котором __aiter__ возвращал ожидаемый объект. Современный протокол требует непосредственного возврата асинхронного итератора, что делает поведение однозначным.
async for должен понять, у какого объекта вызвать следующий шаг итерации. Если __aiter__ возвращает корутину, результатом является ожидаемый объект, а не объект с корректным __anext__.
Неверная реализация может проявиться только во время выполнения асинхронного цикла. Это особенно опасно при миграции старого кода: функция выглядит асинхронной и возвращает корутину, но не соответствует современному протоколу.
При выполнении async for Python вызывает __aiter__ у итерируемого объекта. Этот метод обязан вернуть асинхронный итератор. Затем для каждого шага Python вызывает его __anext__, ожидает возвращённый результат и получает очередное значение.
Завершение итерации обозначается исключением StopAsyncIteration, а не обычным StopIteration. Поэтому асинхронный итератор обычно имеет синхронный __aiter__ и асинхронный __anext__.
Здесь __aiter__ сразу возвращает экземпляр Numbers, а __anext__ является корутиной и возвращает ожидаемый результат. Если сделать __aiter__ асинхронным методом, он вернёт корутину и нарушит современный контракт.
Асинхронный итератор может выполнять ожидание внутри __anext__: например, ждать поступления очередного сообщения. Это не означает, что ожидание допустимо внутри __aiter__.
Сервис постранично читает данные из API и предоставляет их потребителю через async for. Разработчик сделал __aiter__ асинхронным, потому что первая страница загружается по сети именно там. В результате цикл получает корутину вместо асинхронного итератора и завершается ошибкой.
Вариант с загрузкой первой страницы в __aiter__ нарушает протокол. Вариант с предварительным отдельным вызовом загрузки формально возможен, но усложняет интерфейс и повышает риск неправильного порядка вызовов.
Выбранное решение — оставить __aiter__ обычным методом, возвращающим объект итератора, а загрузку первой и последующих страниц выполнять в __anext__. Это соответствует протоколу, естественно поддерживает ожидание и позволяет циклу единообразно обрабатывать все страницы.
1. Чем асинхронный итератор отличается от асинхронно итерируемого объекта?
Асинхронно итерируемый объект предоставляет __aiter__. Результат вызова __aiter__ должен быть асинхронным итератором, то есть объектом с __anext__. Эти роли могут быть реализованы одним объектом, как в примере, но это не обязательное требование.
2. Как обозначается завершение асинхронной итерации?
Метод __anext__ должен возбуждать StopAsyncIteration. Если он вернёт это исключение как обычное значение или возбудит StopIteration, цикл не получит корректного сигнала завершения; StopIteration внутри корутины дополнительно преобразуется в RuntimeError.
3. Можно ли использовать асинхронный генератор как источник для async for?
Да. Асинхронный генератор автоматически реализует нужный протокол: его __aiter__ возвращает асинхронный итератор, а получение очередного значения выполняется через ожидание. Однако асинхронный генератор нельзя использовать с обычным for, поскольку он не обязан реализовывать синхронный метод __iter__ и синхронный __next__.