Сопрограмма или корутина — это генератор. Однако в PEP-342 предложили расширить возможности генераторов, добавив туда несколько конструкций, о которых сейчас пойдёт речь.
Одна из ключевых особенностей корутин — возможность приостановить выполнение функции до получения данных, сохранив контекст. Самая простая корутина выглядит так:
def coro():
val = (yield)
print(val)
c = coro() # Объявляем корутину
next(c) # Просим выполнить метод до первого `yield`
c.send(12) # Передаём данные в корутину
c.close() # Завершаем корутинуВ результате в консоли появится 12 и сообщение об ошибке GeneratorExit: корутина получила данные через метод send, вывела их на печать и завершилась.
Корутины могут не только отдавать значения и запоминать место, где остановился код, но и ждать новых значений. Для этого есть конструкция (yield), которая позволяет принимать набор параметров. Так как приём параметров в корутине происходит необычным способом, то их отправка сделана с помощью специальной функции send(...). В конце можно вызвать метод close(), который прекратит выполнение корутины. Когда вы вызываете метод close(), выбрасывается исключение GeneratorExit, которое можно перехватить и грамотно обработать.
Рассмотрим пример посложнее: представьте себе метод, который на вход получает какое-то значение, обрабатывает его и отдаёт результат. Пусть это будет функция, рассчитывающая количество денег на вашем счёте через N лет при определённой процентной ставке. На вход функция принимает годовую процентную ставку по депозиту и сумму на счёте.
import math
def cash_return(deposit: int, percent: float, years: int) -> float:
value = math.pow(1 + percent / 100, years)
return round(deposit * value, 2)Узнаем, сколько вы получите денег через 5 лет, если сумма, лежащая на депозите — 1 000 000 рублей, а ставка по депозиту — 5% годовых.
>>> cash_return(1_000_000, 5, 5)
1276281.56Получается достаточно приличная сумма!
Теперь вы хотите посмотреть на то, как будет меняться итоговая сумма в зависимости от размера депозита. Тут приходит на помощь корутина.
import math
def cash_return_coro(percent: float, years: int) -> float:
value = math.pow(1 + percent / 100, years)
while True:
try:
deposit = (yield)
yield round(deposit * value, 2)
except GeneratorExit:
print('Выход из корутины')
raiseЗапустим корутину с теми же условиями — 5 лет и 5% годовых.
coro = cash_return_coro(5, 5)
next(coro)
values = [1000, 2000, 5000, 10000, 100000]
for item in values:
print(coro.send(item))
next(coro)
coro.close()Получим:
1276.28
2552.56
6381.41
12762.82
127628.16
Выход из корутиныВ функции cash_return_coro нет необходимости вычислять переменную value каждый раз, когда вы хотите посчитать сумму. Недостатком такого подхода можно назвать большее количество кода, необходимого для корректной работы.
Применение корутин
Напишем небольшую реализацию корутины, которая умеет хранить историю результатов выполнения функции и выводить результат.
import operator
def func_manager():
history = []
while True:
x, y, func = (yield)
if func == "h":
print(history)
continue
result = func(x, y)
print(result)
history.append(result)
manager = func_manager()
print(type(manager))
manager.send(None)
manager.send((1, 2, operator.add))
manager.send((100, 20, operator.sub))
manager.send((5, 15, operator.mul))
manager.send((None, None, "h"))
manager.close()>> output
<class 'generator'>
3
80
75
[3, 80, 75]Создали генератор и проинициализировали его вызовом manager.send(None), далее подаём входные данные, а результат выполнения сохраняем в переменную.
Теперь упростим программу и избавимся от необходимости инициализировать генератор. Для решения этой проблемы используем декоратор.
import operator
def coroutine(f):
def wrap(*args,**kwargs):
gen = f(*args,**kwargs)
gen.send(None)
return gen
return wrap
@coroutine
def func_manager():
history = []
while True:
x, y, func = (yield)
if func == "h":
print(history)
continue
result = func(x, y)
print(result)
history.append(result)
manager = func_manager()
print(type(manager))
manager.send((1, 2, operator.add))
manager.send((100, 20, operator.sub))
manager.send((5, 15, operator.mul))
manager.send((None, None, "h"))
manager.close()Получили пример, как можно писать свои сопрограммы.
Преимущества использования корутин
Эффективность. Многозадачная программа эффективнее расходует ресурсы. Основной код не блокируется, чтобы мог выполниться вспомогательный модуль. Вместо этого они работают асинхронно.
Удобство для пользователя. Корутины переключаются быстро, поэтому для пользователя это выглядит как одновременное выполнение задач. Ему не приходится подолгу ждать ответа программы. Он продолжает работать с ней, пока асинхронные корутины незаметно для него выполняют дополнительные действия.
Снижение нагрузки на систему. Асинхронность позволяет выполнять несколько действий в рамках одного потока вместо того, чтобы множить потоки. Системе проще выполнить один поток, чем несколько.
Гибкость в управлении. Переключение между корутинами происходит вручную. Программист сам прописывает этот момент в коде, поэтому управление корутинами можно контролировать. Нет риска, что программа переключится между блоками кода в неподходящий момент, как это бывает с менее гибкими решениями.
Недостатки сопрограмм
Сложность и высокий порог вхождения. Разобраться в работе корутин и научиться писать асинхронный код может быть сложно. Поэтому сопрограммы начинают изучать специалисты, которые уже хорошо разобрались в базовых принципах выбранного языка.
Узкая специализация. Корутины — высокоуровневое решение. Ускорить сложные вычисления с помощью сопрограмм не получится. В этом случае нужна многопоточность, а не асинхронность. При этом корутины подходят для снижения нагрузки на систему.
Что нужно запомнить про корутины
Достоинства корутин в Python:
Корутина — механизм асинхронного взаимодействия, управляемый программно, а не системно в отличие от потоков или процессов.
Корутина менее ресурсоёмкая, чем потоки.
Корутина более гибкая в использовании, чем потоки.
Корутина не ускоряет программу, но помогает сделать её работу более оптимальной.
Недостатки корутин в Python:
Высокий порог вхождения в понимание темы.
Повышенный уровень включённости в работу программы и её дальнейшая поддержка.