что такое await в python
Поддержка асинхронности async/await в Python.
Внимание! Любой асинхронный код написанный на языке Python не будет работать без поддержки его распараллеливания во время выполнения.
Asyncio используется в качестве основы для нескольких асинхронных сред Python, которые предоставляют высокопроизводительные сетевые и веб-серверы, библиотеки подключений к базам данных, распределенные очереди задач и т. д.
Ключевое слово await позволяет сопрограмме отдать контроль назад в главный цикл, который содержит порядок исполнения всех сопрограмм.
Сопрограммы/корутины async def в Python.
Внутри тела функции сопрограммы идентификаторы await и async становятся зарезервированными ключевыми словами. Выражения await, async for и async with могут использоваться только в телах функций сопрограмм.
Асинхронный async for в Python.
Асинхронный контекст-менеджер async with в Python.
Асинхронный менеджер контекста может приостановить выполнение в своих методах `__aenter__()` и `__aexit__()`. Асинхронный оператор `with` может использоваться только в теле асинхронной функции (сопрограммы).
Асинхронный итератор в Python.
Асинхронный итератор может вызывать асинхронный код в своем методе `__anext__` и могут использоваться только в асинхронном операторе `async for`.
Асинхронный генератор в Python.
Наличие выражения yield в функции или методе, определенном с использованием async def, дополнительно определяет функцию как функцию асинхронного генератора.
Асинхронность в Python
Содержание статьи
Словарик терминов
Что важно знать?
Асинхронность — не то же самое, что и параллельность.
Конкурентность — разбиение задач на блоки и определение того, как будет осуществляться переключение между этими задачами. Другими словами, 2 большие задачи может выполнять один и тот же работник, переключаясь между ними.
Благодаря конкурентности можно поставить одновременно выполняться 2 задачи: большую и маленькую, и маленькая выполнится быстрее, независимо от того, в каком порядке они были вызваны.
Параллельность — выполнение 2 задач одновременно. Один работник не может выполнять параллельно 2 задачи, поэтому используем несколько (например, несколько ядер процессоров)
Еще есть работа в тредах (Threads), или на русском, потоках
В обеих библиотеках есть потоки, где и выполняется код, но отличие Asyncio в том, что тут вся работа явно выполняется в одном потоке (объекте loop)!
И место, где осуществлять передачу управления другой задаче определяет именно программист. В asyncio именно задачи выполняются конкурентно, но используют они общий поток.
А в тредах потоков может быть несколько, и решение какому потоку передать управление — принимает операционная система (ОС)
Из-за этого существуют проблемы с тем, как и какие данные используются в потоках и могут возникать сложно-отслеживаемые
баги и race-condition.
CPU-bound vs I/O-bound
В программировании часто различают эти два типа задач, чтобы определить правильный подход к оптимизации кода:
Библиотека Asyncio (от слов Async и I/O) решает проблему ввода-вывода с помощью асинхронного переключения между
такими задачами. Теперь вечный цикл работает не на одну задачу, а на все сразу.
Для того чтобы сделать это возможным, программист «делит» свою программу на подзадачи и «говорит» где одной функции
приостановиться, а где другой.
Абстрактный пример №1:
В жаркий летний день, перед тем как отправиться домой, курьер доставки взял 2 (точки «B» и «D») заказа, которые
находятся по разные стороны от него. Доставлять заказы он будет на велосипеде.
При этом, по пути к «B», нужно заехать в пиццерию (точка «A»), а по пути к заказу «D» в ресторан (точка «C»).
По пути к точке «A» он вспоминает что нужно домой купить продукты, но он не знает какие. Курьер знает, что где-то на
половине маршрута между каждыми 2-мя точками он может приостановиться. Он приостанавливается в точке «F» и отправляет
сообщение жене:
Забрав заказ из пиццерии («A»), по дороге к пункту «B», курьер останавливается в продуктовом магазине («G»), покупает
продукты и продолжает свой путь к точке «B».
После того как он выполнил заказ, он направляется в магазин вин (точка «H») и покупает бутылочку вина, а потом забирает
заказ из ресторана (точка «C») и отвозит его клиенту («D»)
В конце концов, курьер направляется домой в точку «I».
В данном случае, курьер, выполняет 3 отдельные задачи:
Отличие этих задач в том, что доставку заказов «B», «D» он выполняет последовательно и не может одновременно
двигаться в 2 пункта назначения (предположим, они находятся в разных частях города для наглядности).
Но задачу «E» (доставку домой продуктов) курьер может выполнять конкурентно к этим задачам. Заметьте, что для того, чтобы выполнить какую-то из подзадач задачи «E»* например, «G»), курьеру нужно либо просто приостановиться, либо приостановиться и отклониться от маршрута.
Это работает и в обратную сторону, курьер не может одновременно зайти в продуктовый и в пиццерию. Поэтому, одна из задач должна приостановиться.
С одной стороны, курьер тормозит выполнение заказа своему клиенту, но успевает выполнить поручение жены. В ином случае, если бы курьер сначала выполнил все заказы, а только потом поехал за продуктами, то он бы приехал домой позже, поэтому он решил совместить задачи [«B», «D»] с задачей [«E»].
Эти задачи он совместил КОНКУРЕНТНО.
Рассмотрим другой вариант, когда курьер берет 2 заказа, но отдает второй напарнику, который выезжает с другой стороны
сразу в точку «C».
Он все еще может соединить выполнение задач «B» и «E» конкурентно, но выполнение «B» и «D» он совместил параллельно, использовав второго курьера!
Абстрактный пример №2
В шахматах есть такая форма игры, когда один шахматист одновременно играет с несколькими противниками. Это называется «Сеанс одновременной игры».
Участнику приходится переключаться между разными партиями на время приостанавливая игру на других столах. Он «решает» эти «задачи» конкурентно.
Для того чтобы «решать» их параллельно нужно просто-напросто несколько игроков.
Более подробно этот пример описан тут.
Асинхронность в Python
На параллельной работе мы останавливаться не будем, сразу перейдем к асинхронной работе.
Очень важно, чтобы к этому моменту вы понимали, как работают функции и генераторы
В стандартном примере, функция может возвращать по выполнению какой-то задачи какое-то значение:
Каждый раз, когда вы будете вызывать эту функцию, вы обязаны передать в нее значения ‘a’, ‘b’ и вы получите на выходе
результат их умножения:
Генераторы же позволяют отдавать разные значения при «повторном» их вызове.
Еще одно отличие генератора от функции: когда происходит вызов функции-генератора, то создается объект генератора. (В случае с простой функцией, у нас возвращался результат ее выполнения).
Попробуем на примере:
В примере выше происходит следующее:
То, что нам и нужно!
Теперь к библиотеке Asyncio
Ранее вы могли встречать другой синтаксис типа декораторов @asyncio.coroutine над обычными функциями, а аналогом await были слова yield from
Теперь, когда вы хотите создать нативную корутину для работы с асинхронностью (не асинхронный генератор, это другое) вы должны делать это так:
А само слово await позволяет выполнить другую корутину и передать управление дальше. То есть, чтобы сделать ваш синхронный код асинхронным, вам недостаточно просто сделать приставку async перед функцией, а необходимо еще в каком-либо месте передать управление дальше.
Рассмотрим конкурентное выполнение на примере
Сделаем обычный счетчик и 3 таймера, которые будут срабатывать:
Для начала, нам надо импортировать библиотеку asyncio
Теперь начнем писать асинхронную функцию счетчика.
Для того чтобы была возможность выводить значение счетчика в другой асинхронной функции, нам нужно будет создать общий объект. Его я создам чуть позже в общей функции, но чтобы он был действительно общий для 2-х корутин, я буду использовать объект списка и приму его в эту функцию.
После чего я запущу вечный цикл и внутри буду делать паузу на одну тысячную секунды, а потом добавлять один элемент в список. Т.е. у меня цикл будет передавать управление (после каждого отсчета) другим корутинам.
Исходя из этого расчета, у меня за одну секунду не может быть проведено больше 1000 операций, так как пауза делается 1000 раз в секунду. Остальное время отнимется на выполнение других операций в других корутинах.
Напоминаю:
Теперь пропишем функции таймеров, которые будут срабатывать каждые 1, 5, 10 секунд. Разве что, первый таймер еще примет в себя объект счетчика и скажет сколько там сейчас элементов.
Как видно в коде, теперь в каждой функции asyncio.sleep принимает в себя то количество секунд, которое нам нужно.
Теперь нам необходимо создать функцию, которой мы будем запускать все предыдущие функции.
Подробнее про запуск асинхронных тасков можно почитать тут (англ.)
Все вместе выглядит как-то так:
Запускаем и наслаждаемся результатом:
Все работает так, как и задумано. Вроде бы функции работают параллельно, но на самом деле конкурентно, они просто передают друг другу управление, просто «говоря» общему потоку:
В примере выше, количество записей в списке через 10 секунд не превышает 10 000, т.е. функция подсчета и правда сработала не чаще.
Итого, за 10 секунд меньше 1000 операций, как мы и хотели.
Назад в Содержание
Блокирующие или неблокирующие асинхронные функции
Важно понимать один момент: если вы просто пытаетесь запустить корутины одну за одной, то они будут выполняться последовательно, а не конкурентно.
Они будут выполняться как в обычном синхронном блокирующем коде.
Пример:
Если вы сначала запустите корутину await count(counter) (как в примере выше), то она так и продолжит выполняться, а корутины-счетчики не запустятся никогда.
А вот именно с помощью asyncio.gather мы «собираем» все корутины и запускаем их конкурентно.
Кстати говоря, с помощью asyncio.gather у нас каждая переданная корутина формируется в новую задачу (Task), а эти таски уже имеют свой «контекст», где можно использовать контекстные переменные.
Контекстные переменные
Учитывая, что разные задачи выполняются конкурентно, но всё-таки отдельно, в Python есть такое понятие как «контекст».
Если вы в одной корутине вызываете другую, то они делят между собой один контекст, и в этот контекст можно размещать отдельные переменные, чтобы потом их доставать там, где вам это понадобится.
Контекстные переменные — это что-то вроде глобальных переменных, только доступных не во всем коде, а только в необходимом контексте.
Это бывает полезно, когда вы не хотите передавать переменную в другую функцию явно, например, если таких переменных у вас много и вы наверняка не знаете, где именно вам может понадобиться та или иная переменная.
Пример:
Сделаем функцию, которая будет увеличивать наш счетчик:
Теперь сделаем функцию-цикл, где будет вызываться метод increase :
Результат:
В данном примере это не совсем оправдывает себя, но если вложенность у вас большая, то такой способ бывает полезен.
Пример с разными контекстами
Давайте примем в функцию count произвольную задержку и запустим 2 разных контекста с помощью функции asyncio.gather
Результат:
Как видно на картинке, счетчики работают конкурентно, иногда какой-то счетчик срабатывает быстрее другого, и значения в них независимы друг от друга.
Опасность
В python-комьюнити не любят, когда в асинхронном фреймворке (вроде aiogram) используют блокирующие синхронные функции, так как смысл от асинхронности теряется.
Попробуем добавить time.sleep в одном из контекстов выше и посмотрим на результат.
Результат
Помимо этого, произошла еще одна вещь, счетчики синхронизировались (с разницей в 1 сек), потому что блокирующая задержка больше, и обе задачи готовы к запуску сразу же после нее.
Асинхронное программирование в Python
Авторизуйтесь
Асинхронное программирование в Python
Асинхронное программирование на Python становится все более популярным. Для этих целей существует множество различных библиотек. Самая популярная из них — Asyncio, которая является стандартной библиотекой Python 3.4. Из этой статьи вы узнаете, что такое асинхронное программирование и чем отличаются различные библиотеки, реализующие асинхронность в Python.
По очереди
В каждой программе строки кода выполняются поочередно. Например, если у вас есть строка кода, которая запрашивает что-либо с сервера, то это означает, что ваша программа не делает ничего во время ожидания ответа. В некоторых случаях это допустимо, но во многих — нет. Одним из решений этой проблемы являются потоки (threads).
Потоки дают возможность вашей программе выполнять ряд задач одновременно. Конечно, у потоков есть ряд недостатков. Многопоточные программы являются более сложными и, как правило, более подвержены ошибкам. Они включают в себя такие проблемы: состояние гонки (race condition), взаимная (deadlock) и активная (livelock) блокировка, исчерпание ресурсов (resource starvation).
Переключение контекста
Хотя асинхронное программирование и позволяет обойти проблемные места потоков, оно было разработано для совершенно другой цели — для переключения контекста процессора. Когда у вас есть несколько потоков, каждое ядро процессора может запускать только один поток за раз. Для того, чтобы все потоки/процессы могли совместно использовать ресурсы, процессор очень часто переключает контекст. Чтобы упростить работу, процессор с произвольной периодичностью сохраняет всю контекстную информацию потока и переключается на другой поток.
Асинхронное программирование — это потоковая обработка программного обеспечения / пользовательского пространства, где приложение, а не процессор, управляет потоками и переключением контекста. В асинхронном программировании контекст переключается только в заданных точках переключения, а не с периодичностью, определенной CPU.
Эффективный секретарь
Теперь давайте рассмотрим эти понятия на примерах из жизни. Представьте секретаря, который настолько эффективен, что не тратит время впустую. У него есть пять заданий, которые он выполняет одновременно: отвечает на телефонные звонки, принимает посетителей, пытается забронировать билеты на самолет, контролирует графики встреч и заполняет документы. Теперь представьте, что такие задачи, как контроль графиков встреч, прием телефонных звонков и посетителей, повторяются не часто и распределены во времени. Таким образом, большую часть времени секретарь разговаривает по телефону с авиакомпанией, заполняя при этом документы. Это легко представить. Когда поступит телефонный звонок, он поставит разговор с авиакомпанией на паузу, ответит на звонок, а затем вернется к разговору с авиакомпанией. В любое время, когда новая задача потребует внимания секретаря, заполнение документов будет отложено, поскольку оно не критично. Секретарь, выполняющий несколько задач одновременно, переключает контекст в нужное ему время. Он асинхронный.
Потоки — это пять секретарей, у каждого из которых по одной задаче, но только одному из них разрешено работать в определенный момент времени. Для того, чтобы секретари работали в потоковом режиме, необходимо устройство, которое контролирует их работу, но ничего не понимает в самих задачах. Поскольку устройство не понимает характер задач, оно постоянно переключалось бы между пятью секретарями, даже если трое из них сидят, ничего не делая. Около 57% (чуть меньше, чем 3/5) переключения контекста были бы напрасны. Несмотря на то, что переключение контекста процессора является невероятно быстрым, оно все равно отнимает время и ресурсы процессора.
Зеленые потоки
Зеленые потоки (green threads) являются примитивным уровнем асинхронного программирования. Зеленый поток — это обычный поток, за исключением того, что переключения между потоками производятся в коде приложения, а не в процессоре. Gevent — известная Python-библиотека для использования зеленых потоков. Gevent — это зеленые потоки и сетевая библиотека неблокирующего ввода-вывода Eventlet. Gevent.monkey изменяет поведение стандартных библиотек Python таким образом, что они позволяют выполнять неблокирующие операции ввода-вывода. Вот пример использования Gevent для одновременного обращения к нескольким URL-адресам:
Как видите, API-интерфейс Gevent выглядит так же, как и потоки. Однако за кадром он использует сопрограммы (coroutines), а не потоки, и запускает их в цикле событий (event loop) для постановки в очередь. Это значит, что вы получаете преимущества потоков, без понимания сопрограмм, но вы не избавляетесь от проблем, связанных с потоками. Gevent — хорошая библиотека, но только для тех, кто понимает, как работают потоки.
Давайте рассмотрим некоторые аспекты асинхронного программирования. Один из таких аспектов — это цикл событий. Цикл событий — это очередь событий/заданий и цикл, который вытягивает задания из очереди и запускает их. Эти задания называются сопрограммами. Они представляют собой небольшой набор команд, содержащих, помимо прочего, инструкции о том, какие события при необходимости нужно возвращать в очередь.
Функция обратного вызова (callback)
В Python много библиотек для асинхронного программирования, наиболее популярными являются Tornado, Asyncio и Gevent. Давайте посмотрим, как работает Tornado. Он использует стиль обратного вызова (callbacks) для асинхронного сетевого ввода-вывода. Обратный вызов — это функция, которая означает: «Как только это будет сделано, выполните эту функцию». Другими словами, вы звоните в службу поддержки и оставляете свой номер, чтобы они, когда будут доступны, перезвонили, вместо того, чтобы ждать их ответа.
Давайте посмотрим, как сделать то же самое, что и выше, используя Tornado:
В примере вы можете заметить, что первая строка функции handle_response проверяет наличие ошибки. Это необходимо, потому что невозможно обработать исключение. Если исключение было создано, то оно не будет отрабатываться в коде из-за цикла событий. Когда fetch выполняется, он запускает HTTP-запрос, а затем обрабатывает ответ в цикле событий. К тому моменту, когда возникнет ошибка, стек вызовов будет содержать только цикл событий и текущую функцию, при этом нигде в коде не сработает исключение. Таким образом, любые исключения, созданные в функции обратного вызова, прерывают цикл событий и останавливают выполнение программы. Поэтому все ошибки должны быть переданы как объекты, а не обработаны в виде исключений. Это означает, что если вы не проверили наличие ошибок, то они не будут обрабатываться.
Другая проблема с обратными вызовами заключается в том, что в асинхронном программировании единственный способ избегать блокировок — это обратный вызов. Это может привести к очень длинной цепочке: обратный вызов после обратного вызова после обратного вызова. Поскольку теряется доступ к стеку и переменным, вы в конечном итоге переносите большие объекты во все ваши обратные вызовы, но если вы используете сторонние API-интерфейсы, то не можете передать что-либо в обратный вызов, если он этого не может принять. Это также становится проблемой, потому что каждый обратный вызов действует как поток. Например, вы хотели бы вызвать три API-интерфейса и дождаться, пока все три вернут результат, чтобы его обобщить. В Gevent вы можете это сделать, но не с обратными вызовами. Вам придется немного поколдовать, сохраняя результат в глобальной переменной и проверяя в обратном вызове, является ли результат окончательным.
Сравнения
Если вы хотите предотвратить блокировку ввода-вывода, вы должны использовать либо потоки, либо асинхронность. В Python вы выбираете между зелеными потоками и асинхронным обратным вызовом. Вот некоторые из их особенностей:
Зеленые потоки
Обратный вызов
Как решить эти проблемы?
Прим. перев. В примерах используется aiohttp версии 1.3.5. В последней версии библиотеки синтаксис другой.
Несколько особенностей, которые нужно отметить:
Единственная проблема заключается в том, что объект выглядит как генератор, и это может вызвать проблемы, если на самом деле это был генератор.
Async и Await
Заключение
В Python встроена отличная асинхронная библиотека. Давайте еще раз вспомним проблемы потоков и посмотрим, решены ли они теперь:
Несмотря на то, что Asyncio довольно хорош, у него есть и проблемы. Во-первых, Asyncio был добавлен в Python недавно. Есть некоторые недоработки, которые еще не исправлены. Во-вторых, когда вы используете асинхронность, это значит, что весь ваш код должен быть асинхронным. Это связано с тем, что выполнение асинхронных функций может занимать слишком много времени, тем самым блокируя цикл событий.
Существует несколько вариантов асинхронного программирования в Python. Вы можете использовать зеленые потоки, обратные вызовы или сопрограммы. Хотя вариантов много, лучший из них — Asyncio. Если используете Python 3.5, то вам лучше использовать эту библиотеку, так как она встроена в ядро python.
Async/await: асинхронные возможности в Python 3+
Содержание статьи
Цикл передач на третьем канале
16 марта 2014 года произошло событие, которое привело к довольно бодрым холиварам, — вышел Python 3.4, а вместе с ним и своя внутренняя реализация event loop’а, которую окрестили asyncio. Идея у этой штуки была ровно такая, как я написал во введении: вместо того чтобы зависеть от внешних сишных реализаций отлова неблокирующих событий на сокетах (у gevent — libevent, у Tornado — IOLoop и так далее), почему бы не встроить одну в сам язык?
Правда, сами разработчики отнеслись к новой спецификации довольно неоднозначно и с опаской. Хоть код и старался быть максимально совместимым по синтаксису со второй версией языка — проект tulip, который как раз был первой реализацией PEP 3156 и лег в основу asyncio, был даже в каком-то виде бэкпортирован на устаревшую (да-да, я теперь ее буду называть только так) двойку.
Дело было еще и в том, что реализация, при всей ее красоте и приверженности дзену питона, получилась довольно неторопливая. Разогнанные gevent и Tornado все равно оказывались на многих задачах быстрее. Хотя, раз уж в народ в комьюнити настаивал на тюльпанах, в Tornado таки запилили экспериментальную поддержку asyncio вместо IOLoop, пусть она и была в разы медленнее. Но нашлось у новой реализации и преимущество — стабильность. Пусть соединения обрабатывались дольше, зато ответа в итоге дожидалась бОльшая доля клиентов, чем на многих других прославленных фреймворках. Да и ядро при этом, как ни странно, нагружалось чуть меньше.
Но не все сразу заметили, что над новой версией Python завис великий и ужасный PEP 492.
Сегодня в сопрограмме
Так уж получилось, что довольно большое число людей изначально не до конца поняло смысл введения asyncio и считало его чем-то наподобие gevent, то есть сетевым или даже веб-фреймворком. Но суть у него была совсем другая — он открывал новые возможности асинхронного программирования в ядре языка.
Ты же помнишь в общих чертах, что такое генераторы и корутины (они же сопрограммы)? В контексте Python можно привести два определения генераторов, которые друг друга дополняют:
Корутины же всегда определялись как генераторы, которые, помимо того что вычисляли значения на каждом этапе, могли принимать на каждом обращении параметры, используемые для расчетов следующей итерации. По сути, это и есть вычислительные единицы в контексте того, что называют кооперативной многозадачностью, — можно сделать много таких легковесных корутин, которые будут очень быстро передавать друг другу управление.
В случае сетевого программирования именно это и позволяет нам быстро опрашивать события на сокете, обслуживая тысячи клиентов сразу. Ну или, в общем случае, мы можем написать асинхронный драйвер для любого I/O-устройства, будь то файловая система на block device или, скажем, воткнутая в USB Arduino.
Да, в ядре Python есть пара библиотек, которые изначально предназначались для похожих целей, — это asyncore и asynchat, но они были, по сути, экспериментальной оберткой над сетевыми сокетами, и код для них написан довольно давно. Если ты сейчас, в начале 2017 года, читаешь эту статью — значит, настало время записать их в музейные экспонаты, потому что asyncio лучше.
Давай забудем на время про несвежий Python 2 и взглянем на реализацию простейшего асинхронного эхо-сервера в Python 3.4:
Да, этот код асинхронный, но callback hell — тоже вещь довольно неприятная. Немного неудобно описывать асинхронные обработчики как гроздья висящих друг на друге колбэков, не находишь? Отсюда и проистекает тот самый классический вопрос: как же нам, кабанам, писать асинхронный код, который не был бы похож на спагетти, а просто выглядел бы несложно и императивно? На этом месте передай привет в камеру ноутбука (если она у тебя не заклеена по совету ][) тем, кто активно использует Twisted или, скажем, пишет на JavaScript, и поехали дальше.
А теперь давай возьмем Python 3.5 (давно пора) и напишем все на нем.
Несложно заметить, что в случае асинхронного программирования подобным образом в питоне все будет крутиться (каламбур) вокруг того самого внутреннего IOLoop’а, который будет связывать события с их обработчиками. Одной из основных проблем, как я уже говорил, остается скорость — связка Python 2 + gevent, которая использует крайне быстрый libev, по производительности показывает гораздо лучшие результаты.
Но зачем держаться за прошлое? Во-первых, есть curio (см. врезку), а во-вторых, уже есть еще одна, гораздо более скоростная реализация event loop’а, написанная как подключаемый плагин для asyncio, — uvloop, основанный на адски быстром libuv.
Что, уже чувствуешь ураганный ветер из монитора?
Тетя Ася может все
Итак, что же мы имеем? Мы имеем асинхронные функции, они же корутины. Вот такие:
Если мы просто так возьмем и вызовем эту функцию, ничего не произойдет, потому что нам вернется ленивая корутина. Но мы же помним из статей о генераторах, что нам нужно сделать, чтобы ее запустить? Правильно — передать ей контекст через оператор yield. Формально этого yield’а у нее нет, но мы можем послать в нее значение для того, чтобы «промотать» корутину до следующего переключения контекста:
Да, мы ее просто «подождем», как маму из той самой песни. Таким образом мы можем выстроить целый разветвленный граф из корутин, которые «ожидают» друг друга и передают управление туда и обратно. Если ты сейчас вскочил с кресла и воскликнул: «Да это же кооперативная многозадачность!» — молодец, к этому все и шло.
Кстати, если все равно назло маме вызвать функцию без await внутри корутины, то нам не просто вернется coroutine object, но еще и в консоль упадет большой warning и напоминание coroutine ‘blablabla’ was never awaited. Ее никто не дождался, поэтому она обиделась и не стала исполняться. Но такие сообщения очень помогают в отладке.
А еще — нельзя просто так взять и вызвать await в интерактивном REPL’е, потому что он не является корутиной сам по себе:
В остальных случаях await можно писать где угодно внутри корутины, за исключением списковых включений (они же list comprehensions, и это обещают добавить в ближайших релизах) и лямбд (потому что они сами не корутины). А async можно использовать, например, для методов в классе (за исключением «приватных» методов, которые могут дергаться самим Python’ом, понятия не имеющим, что у вас там корутина).
Давай напишем, как нам теперь реально запустить всю эту катавасию:
Все довольно просто: мы достаем event loop и заставляем корутину запуститься в нем. Много кода, скажешь? Не особо на самом деле, особенно с учетом того, какие преимущества это нам дает.
Ближе к жизни
Я мог бы рассказать еще про такие штуки, как async for и async with:
Но лучше почитай про всякую глубинную магию по ссылкам во врезке, а сейчас давай обратимся к более практическому примеру.
Есть банальная, казалось бы, задача, которая практически нереализуема во втором питоне, — запустить подпроцесс и асинхронно читать его вывод по мере поступления, как, собственно, и должен работать PIPE.
В последнее время я сильно разочаровался во встроенном модуле subprocess, но к нам на помощь спешит асинхронная реализация, на данный момент часть asyncio. И там это делается просто и красиво.
Да, это много кода. Но он должен казаться гораздо более понятным после объяснений выше, да и вообще он довольно легко читается. Я, честно сказать, искренне надеюсь, что именно возможности наподобие описанных позволят наконец большему количеству народа распробовать Python 3.5 и перейти на него окончательно.
Сухой остаток
Зачем все это нужно? Затем, что слишком много программ рано или поздно упираются в блокировки — когда мы читаем из сокета, когда мы ждем вывод от процесса, когда мы ждем сигнал от устройства и т. д. и т. п. Обычно такие вещи делаются, например, бесконечным циклом — мы будем стучаться, пока не появятся новые данные нам для обработки, а потом условие выполнится и запустится какой-то код.
Так вот, зачем так делать, если мы можем попросить систему саму отправить нам из kernel space (и опять все дружно скажем «Ave epoll!») сообщение о том, что у нас есть новые данные? Не тратя вычислительные ресурсы на ненужный код.
Я думаю, всем любителям питона стоит исследовать этот новый мир, который нам стал доступен совсем недавно и теперь активно развивается. Нам больше не надо патчить модуль socket через gevent и терпеть адские баги. У нас уже есть готовые асинхронные библиотеки для работы с базами данных (например, aiopg), протоколами (aiozmq), сторонними сервисами через API (aiobotocore) и написания скоростных серверов (aiohttp).
Мало ссылок? Ладно, вот еще одна: реализация протокола HTTP2, которую можно гонять хоть на потоках, хоть на корутинах, — очень интересный проект hyper-h2.
Так чего ты еще тут сидишь? Иди пиши код! Удачи!