2025, Oct 03 03:17
Почему await в Python asyncio требует awaitable с __await__
Почему await в Python asyncio работает только с awaitable, чьи __await__ возвращают итератор: роль yield, кооперативная многозадачность и цикл событий.
Почему await в Python asyncio требует итератор
Осваивая asyncio в Python, многие спотыкаются о вопрос: почему await принимает только ожидаемые объекты, которые предоставляют метод __await__, возвращающий итератор. Короткий ответ: асинхронная модель Python — это кооперативная многозадачность на базе генераторов, а yield — механизм, который приостанавливает и продолжает выполнение. Интерфейс итератора — это способ выразить в коде эту паузу и последующее возобновление.
Минимальный пример, из которого возникает вопрос
async def runner():
    data = await rendezvous
    print(data)
class Rendezvous:
    def __await__(self):
        yield "Hello"
        return "World"
rendezvous = Rendezvous()
it = runner().__await__()
first_step = next(it)
Вызов runner() возвращает объект-корутину. Когда корутина доходит до await rendezvous, Python ожидает, что rendezvous — это awaitable. В терминах Python это значит, что у него есть __await__() и этот метод возвращает итератор. Внутренне выражение await rendezvous сводится к вызову rendezvous.__await__() и итерации по полученному итератору.
Что происходит на самом деле
Async — это кооперативная многозадачность. В каждый момент времени выполняется только один фрагмент кода. Прогресс достигается явной передачей управления. Когда одна задача уступает процессор, цикл событий может возобновить другую, а позже вернуться к первой. Речь не о параллельном исполнении на CPU; это про остановку кода, который ждёт, и запуск другого кода в это время.
Эта разница важна, потому что чисто CPU‑связанному коду нет пользы от переключений между задачами. Если вы лишь считаете числа, разбиение работы на корутины чаще всего добавляет накладные расходы на переключение контекста. Async раскрывается, когда задача не упирается в CPU, например сетевой ввод‑вывод или запросы к базе данных. Отправив запрос во внешнюю систему, Python внутри этой задачи не может сделать ничего полезного до получения ответа — по меркам CPU это вечность. В это «праздное» время могут выполняться другие задачи.
Как выразить в коде «приостановиться здесь и продолжить позже»? Базовый примитив — yield. Генераторы дают нужную семантику паузы и возобновления, которой пользуется цикл событий. Поэтому протокол await и описан через итератор: __await__ должен возвращать то, что рантайм может итерировать, а на практике управление осуществляется через точки yield.
Почему __await__ возвращает итератор
Требование итератора существует, потому что именно через yield код на Python кооперативно отдаёт управление. Цикл событий распоряжается тем, кто и когда выполняется, и когда возобновить то, что сделало yield. В чистом Python механизм приостановки и продолжения — это yield. Поэтому протокол генераторов/итераторов — естественное и доступное представление «ожидаемого прогресса».
Есть и практическая граница. Большинство реальных асинхронных примитивов — от сетевых сокетов до драйверов БД — реализованы в низкоуровневых C‑библиотеках. Вы не пишете эти примитивы на чистом Python; вы ими пользуетесь. Модуль asyncio предоставляет часть таких примитивов, чтобы вы могли организовывать их работу. Если же вам нужна awaitable‑логика на уровне Python, __await__ — это точка интеграции с циклом событий, использующая единственный доступный в языке механизм паузы: yield.
Под капотом: как итерация управляет await
Когда интерпретатор выполняет await obj, он получает итератор, вызвав obj.__await__(). Продвигая этот итератор, ожидаемый объект может отдать управление. В момент отдачи цикл событий запускает что‑то ещё. Позже, когда приходит время продолжить, итерация продолжается с позиции последнего yield и в итоге даёт результат, когда итератор завершается. Итератор на базе генератора воплощает жизненный цикл «пауза/продолжение», необходимый циклу событий.
Мог бы await принимать «любой» объект?
В рамках семантики Python нужен способ приостанавливать и возобновлять код. Эту роль выполняет yield, а генераторы открывают yield через протокол итератора. Поэтому await определён через __await__, который возвращает итератор, а не принимает произвольные объекты без возможности yield.
Практический приём: обёртки‑awaitable на уровне Python
Большинство используемых вами awaitable приходят из низкоуровневых C‑модулей. Если вы хотите предоставить «асинхронное» поведение на Python и адаптировать результаты перед возвратом вызывающему коду, это делается через __await__. Точки паузы должны быть выражены через yield, потому что так вы позволяете выполняться другой работе, пока ждёте.
async for record in low_level_stream:
    yield Wrapped(record)
Это иллюстрирует замысел: вы ожидаете результат нижнего уровня и отдаёте адаптированные значения. Суть неизменна: именно yield приостанавливает ваш код и позволяет циклу событий чередовать задачи.
Почему это важно
Понимание того, что async в Python — кооперативный, влияет на проектирование и диагностику систем. Если код никогда не делает yield, ничто другое не выполняется. Если работа упирается в CPU, переключение между корутинами не ускорит её завершение. А когда вам нужны awaitable на уровне Python, связка __await__ плюс yield — это точка интеграции с циклом событий.
Главные выводы
Await нацелен на awaitable, которые предоставляют __await__, возвращающий итератор, потому что генераторы и их yield — это способ, которым код на Python может приостанавливаться и возобновляться. Цикл событий использует этот итератор, чтобы координировать исполнение. Большинство реальных асинхронных примитивов живёт в C, а asyncio даёт к ним доступ, чтобы эффективно организовывать ввод‑вывод и прочие операции, не ограниченные CPU. Когда вам нужно формировать или адаптировать async‑результаты на Python, __await__ — это крючок, а yield — механизм, делающий кооперативную многозадачность возможной.
Статья основана на вопросе на StackOverflow от Meet Patel и ответе deceze.