Программирование PythonМодель данныхPython-разработчик серверных и асинхронных систем

Рассмотрите async for: какое нарушение протокола возникает, если aiter возвращает корутину вместо асинхронн...

Рассмотрите async for: какое нарушение протокола возникает, если __aiter__ возвращает корутину вместо асинхронного итератора?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

В современном Python метод __aiter__ должен сразу вернуть асинхронный итератор — объект с методом __anext__. Если он возвращает корутину, async for не ожидает её автоматически и завершается ошибкой протокола, обычно TypeError.

Исторический контекст

Асинхронная итерация появилась для последовательного получения данных, когда очередной элемент может потребовать ожидания: например, при чтении из сети или асинхронного потока. Протокол разделяет получение итератора и получение следующего элемента, чтобы сам __aiter__ не был лишней точкой ожидания.

В ранних версиях поддержки асинхронных итераторов допускался вариант, при котором __aiter__ возвращал ожидаемый объект. Современный протокол требует непосредственного возврата асинхронного итератора, что делает поведение однозначным.

Постановка проблемы

async for должен понять, у какого объекта вызвать следующий шаг итерации. Если __aiter__ возвращает корутину, результатом является ожидаемый объект, а не объект с корректным __anext__.

Неверная реализация может проявиться только во время выполнения асинхронного цикла. Это особенно опасно при миграции старого кода: функция выглядит асинхронной и возвращает корутину, но не соответствует современному протоколу.

Подробное решение

При выполнении async for Python вызывает __aiter__ у итерируемого объекта. Этот метод обязан вернуть асинхронный итератор. Затем для каждого шага Python вызывает его __anext__, ожидает возвращённый результат и получает очередное значение.

Завершение итерации обозначается исключением StopAsyncIteration, а не обычным StopIteration. Поэтому асинхронный итератор обычно имеет синхронный __aiter__ и асинхронный __anext__.

class Numbers: def __init__(self): self.value = 0 def __aiter__(self): return self async def __anext__(self): if self.value == 3: raise StopAsyncIteration self.value += 1 return self.value async def read(): result = [] async for number in Numbers(): result.append(number) return result

Здесь __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__.