Объект реализует await , возвращая корутину: что произойдёт при await?

Объект реализует __await__, возвращая корутину: что произойдёт при await?

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

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

При выполнении await Python потребует, чтобы __await__ вернул итератор. Если метод возвращает сам объект-корутину, протокол нарушен: выполнение завершится TypeError, поскольку корутина является ожидаемым объектом, но не тем итератором, который должен возвращать __await__.

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

Протокол __await__ появился вместе с моделью async/await в Python 3.5. Он отделяет внешний интерфейс ожидания объекта от конкретной реализации корутины, задачи или примитива планировщика.

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

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

Метод __await__ — это не обычный метод, который должен вернуть любое значение, пригодное для ожидания. Его результат должен поддерживать итерационный протокол: Python использует этот итератор для приостановки и возобновления текущей корутины.

Возврат корутины вместо итератора часто возникает при попытке делегировать ожидание внутреннему асинхронному методу напрямую. В результате ошибка обнаруживается только при выполнении await, а созданная, но не использованная корутина дополнительно может привести к предупреждению о том, что она не была обработана.

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

При обработке выражения await объект Python ищет у объекта специальный метод __await__, вызывает его и проверяет результат. Этот результат должен быть итератором, обычно полученным вызовом __await__() у внутренней корутины, задачи или другого ожидаемого объекта.

Корутина сама по себе поддерживает ожидание, но это не означает, что она является итератором. Поэтому корректная реализация не возвращает внутреннюю корутину напрямую, а делегирует её итератор ожидания:

class Adapter: async def load(self): return 42 def __await__(self): return self.load().__await__()

Вызов self.load() создаёт корутину, а self.load().__await__() возвращает требуемый итератор. Если вместо этого написать return self.load(), при await Adapter() возникнет TypeError.

Практически важно не путать три сущности: корутину как результат вызова async def, ожидаемый объект как объект, допустимый после await, и итератор, который обязан вернуть __await__. Корутина является ожидаемым объектом, но её прямой возврат из __await__ не удовлетворяет требованию к типу результата этого метода.

Ситуация из практики

Сервисный адаптер должен позволять ожидать загрузку данных через await adapter. Рассматривались два варианта: вернуть из __await__ корутину внутреннего метода или вернуть результат её __await__().

Первый вариант проще визуально, но некорректен: он нарушает контракт протокола и приводит к TypeError во время выполнения. Второй вариант корректен, но требует явно понимать границу между корутиной и итератором ожидания.

Если адаптеру не нужно добавлять собственную логику, ещё проще вообще не реализовывать __await__, а предоставить вызываемый асинхронный метод и писать await adapter.load(). Собственный __await__ оправдан, когда сам объект должен представлять одну асинхронную операцию, например обёртку над задачей или ленивым ресурсом. В таком случае делегирование через inner_coroutine().__await__() сохраняет корректность протокола и совместимость с планировщиком.

Что кандидаты часто упускают

  1. Вопрос: Обязан ли __await__ сам быть объявлен как async def?

    Ответ: Нет. Обычно __await__ объявляют обычным методом def, возвращающим итератор. Объявление его через async def изменит результат вызова: метод начнёт возвращать корутину, тогда как Python ожидает от __await__ итератор непосредственно.

  2. Вопрос: Можно ли вернуть из __await__ итератор, который выдаёт произвольные значения?

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

  3. Вопрос: Чем реализация __await__ отличается от реализации __iter__?

    Ответ: __iter__ участвует в синхронной итерации, например в цикле for, а __await__ — только в протоколе await. Наличие __iter__ не делает объект ожидаемым, и наоборот. Это разные протоколы с разными потребителями и разными требованиями к поведению итератора.