Pandora ResearchPandora
Research
RUEN
Python

Корутины в Python

Pandora ResearchPandora Research
18 июня 20267 мин чтения

Сопрограмма или корутина — это генератор. Однако в PEP-342 предложили расширить возможности генераторов, добавив туда несколько конструкций, о которых сейчас пойдёт речь.

Одна из ключевых особенностей корутин — возможность приостановить выполнение функции до получения данных, сохранив контекст. Самая простая корутина выглядит так:

Python
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 лет при определённой процентной ставке. На вход функция принимает годовую процентную ставку по депозиту и сумму на счёте.

Python
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% годовых.

Python
>>> cash_return(1_000_000, 5, 5)
1276281.56

Получается достаточно приличная сумма!

Теперь вы хотите посмотреть на то, как будет меняться итоговая сумма в зависимости от размера депозита. Тут приходит на помощь корутина.

Python
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% годовых.

Python
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()

Получим:

Python
1276.28
2552.56
6381.41
12762.82
127628.16
Выход из корутины

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

Применение корутин

Напишем небольшую реализацию корутины, которая умеет хранить историю результатов выполнения функции и выводить результат.

Python
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()
Python
>> output
<class 'generator'>
3
80
75
[3, 80, 75]

Создали генератор и проинициализировали его вызовом manager.send(None), далее подаём входные данные, а результат выполнения сохраняем в переменную.

Теперь упростим программу и избавимся от необходимости инициализировать генератор. Для решения этой проблемы используем декоратор.

Python
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:

Высокий порог вхождения в понимание темы.

Повышенный уровень включённости в работу программы и её дальнейшая поддержка.

Python

Есть задача для нашей команды?

Расскажите о проекте — оценим и предложим решение в течение одного рабочего дня.

Обсудить проект