Корутины в Python
Когда говорят «написать корутину», обычно подразумевают асинхронную функцию. Корутины можно ставить на паузу, чтобы дать другим функциям немного поработать. В этом заключается принцип асинхронности. О нём мы рассказывали в этой статье.
Давайте сразу рассмотрим пример асинхронной функции:
import asyncio
async def count_to_three():
print("Веду отсчёт. 1")
await asyncio.sleep(0)
print("Веду отсчёт. 2")
await asyncio.sleep(0)
print("Веду отсчёт. 3")
await asyncio.sleep(0)
Очень похоже на обычную функцию, однако здесь есть два новых слова: async
и await
.
async говорит Питону о том, что мы пишем не просто функцию, а асинхронную функцию. Просто добавили async
и всё, функция теперь асинхронная.
Второе слово — await. Оно прерывает исполнение функции, и возвращает управление программой наружу. После этого корутину можно запустить повторно, а затем еще и еще, и каждый раз она будет продолжать работу с того await
, на котором прервалась ранее. Например, в функции count_to_three
команда await
встречается три раза, значит корутину можно вызвать четыре раза (да, не три!). Корутина будет работать до первого await, затем до второго, до третьего и на четвёртый раз выполнит остатки до конца.
Нельзя делать await None
или await "Hello, World!"
. Можно await
только то, что так и называют — «awaitable».
await asyncio.sleep(0)
— это команда корутине
«Дай поработать другим!»
Сразу покажем, как это выглядит на практике:
coroutine_counter = count_to_three()
print(coroutine_counter) # <coroutine object count_to_three at 0x7f5a58486a98>
coroutine_counter.send(None) # Выведет "Веду отсчёт. 1"
coroutine_counter.send(None) # Выведет "Веду отсчёт. 2"
coroutine_counter.send(None) # Выведет "Веду отсчёт. 3"
Мы вызываем асинхронную функцию count_to_three
, однако она не выводит на экран цифру 1, а возвращает корутину. Все асинхронные функции так делают. Это сделано для того, чтобы у вас был объект этой корутины в переменной. Теперь корутину можно запускать раз за разом, а она раз за разом будет делать кусочек и останавливаться на следующем await
.
Чтобы запустить корутину, используют метод send()
. При каждом запуске корутины этим методом она продолжает исполняться с последнего await
, на котором она остановилась. Поэтому при новом запуске той же корутины срабатывает не тот же print
, а следующий.
Нельзя просто .send()
. Всегда нужно передавать какое-то значение. Об этом тоже расскажем позже. Пока что воспринимайте .send(None) как команду «продолжи выполнять корутину».
Когда корутина закончится?
Она остановится навсегда, когда закончатся все await
или встретится return
. Когда корутина заканчивается — она истощается и вызов .send()
выдаёт ошибку:
coroutine_counter = count_to_three()
coroutine_counter.send(None) # Выведет "Веду отсчёт. 1"
coroutine_counter.send(None) # Выведет "Веду отсчёт. 2"
coroutine_counter.send(None) # Выведет "Веду отсчёт. 3"
coroutine_counter.send(None) # Выбросит ошибку StopIteration
Если мы хотим запустить наш счётчик сначала, придётся создать новую корутину, вызвав count_to_three()
:
coroutine_counter = count_to_three()
coroutine_counter.send(None) # Выведет "Веду отсчёт. 1"
coroutine_counter.send(None) # Выведет "Веду отсчёт. 2"
coroutine_counter_new = count_to_three()
coroutine_counter_new.send(None) # Снова выведет "Веду отсчёт. 1", новая корутина
Обычно заранее не известно сколько await
будет до момента «истощения», поэтому исключение приходится «перехватывать»:
coroutine_counter = count_to_three()
while True:
try:
coroutine_counter.send(None) # В четвёртый раз здесь вылетит StopIteration
except StopIteration:
break
Исключение StopIteration
возникает всего один раз. Если после него попробовать запустить корутину ещё раз, то поднимется другое исключение — RuntimeError
, и оно уже будет считаться ошибкой. О том как работать с исключениями читайте в статье про try except.
Нельзя запускать истощённую корутину.
Добиваемся асинхронности
С корутинами разобрались, останавливать их научились. А зачем?..
Корутины позволят вашему коду работать асинхронно, т.е. делать несколько вещей одновременно. Допустим, вы решили скачать несколько файлов. Обычный, синхронный код скачивает файлы по-очереди. Сначала первый файл целиком, затем второй, тоже целиком. Асинхронный код качает файлы одновременно, по кусочкам. Приведём пример скачивания двух файлов:
async def download_file(url):
# здесь происходит какая-то логика со скачиванием файла
image_downloader = download_file('https://www.some-images.com/image1.jpg')
music_downloader = download_file('https://www.music-site.com/artist/album/song5.mp3')
coroutines = [music_downloader, image_downloader]
while True:
for coroutine in coroutines.copy():
try:
coroutine.send(None)
except StopIteration:
coroutines.remove(coroutine)
if len(coroutines) == 0:
break
Разберём как работает код:
-
Мы создали 2 корутины:
image_downloader
иmusic_downloader
. Первая качает картинку по ссылкеhttps://www.some-images.com/image1.jpg
, вторая — музыку по ссыкеhttps://www.music-site.com/artist/album/song5.mp3
. -
Мы положили их в список
coroutines
-
В бесконечном цикле мы по очереди запускаем все корутины из списка. Если вышла ошибка
StopIteration
— корутина истощилась, т.е. файл скачан. Убираем её из списка, корутина больше запускаться не будет. -
Чтобы итерация по списку
coroutines
не сбивалась после удаления элемента из него итерируем не по оригиналу, а по копииcoroutines.copy()
. -
Если список с корутинами закончился (его длина равна нулю), пора заканчивать и бесконечный цикл, потому что все файлы скачаны.
Передать параметры в асинхронную функцию
В плане аргументов асинхронные функции ничем не отличаются от обычных. Доработаем пример со счетчиком и вместо async def count_to_three
напишем универсальную функцию async def count
:
import asyncio
async def count(limit=3):
for step in range(1, limit+1):
print("Веду отсчёт.", step)
await asyncio.sleep(0)
coroutine = count(5)
while True:
coroutine.send(None)
Программа выведет:
Веду отсчёт. 1
Веду отсчёт. 2
Веду отсчёт. 3
Веду отсчёт. 4
Веду отсчёт. 5
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
StopIteration
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.