Сопрограмма в Python
Каждый программист знаком с функциями – последовательностями инструкций, сгруппированных в единое целое для выполнения заранее определенных задач. Они допускают единственную точку входа, способны принимать аргументы, могут иметь или не иметь возвращаемое значение и могут быть вызваны в любой момент во время выполнения программы, в том числе другими функциями и самими собой.
Когда программа вызывает функцию, ее текущий контекст выполнения сохраняется перед передачей управления функции и возобновлением выполнения. Затем функция создает новый контекст – оттуда вновь созданные данные существуют исключительно во время выполнения функций.
Как только задача выполнена, управление передается обратно вызывающей стороне – новый контекст эффективно удаляется и заменяется предыдущим.
Что такое сопрограммы в Python?
Сопрограммы в Python – это особый тип функций, которые сознательно передают контроль вызывающему, но не завершают свой контекст в процессе, а вместо этого поддерживают его в состоянии ожидания.
Они извлекают выгоду из возможности хранить свои данные в течение всего срока службы и, в отличие от функций, могут иметь несколько точек входа для приостановки и возобновления выполнения.
Давайте перейдем к написанию нашей первой сопрограммы:
def bare_bones():
while True:
value = (yield)
Ясно видно сходство с обычной функцией Python. Блок while True: гарантирует непрерывное выполнение сопрограммы до тех пор, пока она получает значения.
Значение собирается с помощью заявления yield.
Понятно, что этот код практически бесполезен, поэтому мы завершим его несколькими операторами печати:
def bare_bones():
print("My first Coroutine!")
while True:
value = (yield)
print(value)
Теперь, что происходит, когда мы пытаемся назвать это так:
coroutine = bare_bones()
Если бы это была обычная функция Python, можно было бы ожидать, что к этому моменту она выдаст какой-то вывод. Но если вы запустите код в его текущем состоянии, вы заметите, что не вызывается ни один print().
Это потому, что сопрограммы требуют, чтобы сначала был вызван метод next():
def bare_bones():
print("My first Coroutine!")
while True:
value = (yield)
print(value)
coroutine = bare_bones()
next(coroutine)
Это запускает выполнение сопрограммы, пока не достигнет своей первой точки останова – value = (yield). Затем он останавливается, возвращая выполнение к основному, и бездействует, ожидая нового ввода:
My first Coroutine!
Новый ввод можно отправить с помощью send():
coroutine.send("First Value")
Затем наше значение переменной получит строку First Value, распечатает ее, и новая итерация цикла while True: заставит сопрограмму еще раз дождаться доставки новых значений. Вы можете делать это сколько угодно раз.
Наконец, когда вы закончите работу с сопрограммой и больше не хотите ее использовать, вы можете освободить эти ресурсы, вызвав close(). Это вызывает исключение GeneratorExit, с которым необходимо иметь дело:
def bare_bones():
print("My first Coroutine!")
try:
while True:
value = (yield)
print(value)
except GeneratorExit:
print("Exiting coroutine...")
coroutine = bare_bones()
next(coroutine)
coroutine.send("First Value")
coroutine.send("Second Value")
coroutine.close()
Вывод:
My first Coroutine! First Value Second Value Exiting coroutine...
Передача аргументов
Как и функции, сопрограммы также могут принимать аргументы:
def filter_line(num):
while True:
line = (yield)
if num in line:
print(line)
cor = filter_line("33")
next(cor)
cor.send("Jessica, age:24")
cor.send("Marco, age:33")
cor.send("Filipe, age:55")
Вывод:
Marco, age:33
Применение нескольких точек прерывания
Несколько операторов yield могут быть упорядочены вместе в одной отдельной сопрограмме:
def joint_print():
while True:
part_1 = (yield)
part_2 = (yield)
print("{} {}".format(part_1, part_2))
cor = joint_print()
next(cor)
cor.send("So Far")
cor.send("So Good")
Вывод:
So Far So Good
Исключение StopIteration
После закрытия сопрограммы повторный вызов send() сгенерирует исключение StopIteration:
def test():
while True:
value = (yield)
print(value)
try:
cor = test()
next(cor)
cor.close()
cor.send("So Good")
except StopIteration:
print("Done with the basics")
Вывод:
Done with the basics
Сопрограммы с декораторами
При работе с более крупными проектами запуск каждой сопрограммы вручную может стать огромным препятствием.
Не волнуйтесь, это просто вопрос использования возможностей декораторов, поэтому нам больше не нужно использовать метод next():
def coroutine(func):
def start(*args, **kwargs):
cr = func(*args, **kwargs)
next(cr)
return cr
return start
@coroutine
def bare_bones():
while True:
value = (yield)
print(value)
cor = bare_bones()
cor.send("Using a decorator!")
Выполнение этого фрагмента кода даст:
Using a decorator!
Конструкция конвейера
Конвейер – это последовательность элементов обработки, организованная таким образом, что выход каждого элемента является входом следующего.
Данные проталкиваются по конвейеру, пока в конечном итоге не будут потреблены. Для каждого трубопровода требуется как минимум один источник и один приемник.
Остальные этапы конвейера могут выполнять несколько различных операций, от фильтрации до изменения, маршрутизации и сокращения данных:

Сопрограммы – естественные кандидаты для выполнения этих операций, они могут передавать данные между собой с помощью операций send(), а также могут выступать в качестве конечных потребителей. Давайте посмотрим на следующий пример:
def producer(cor):
n = 1
while n < 100:
cor.send(n)
n = n * 2
@coroutine
def my_filter(num, cor):
while True:
n = (yield)
if n < num:
cor.send(n)
@coroutine
def printer():
while True:
n = (yield)
print(n)
prnt = printer()
filt = my_filter(50, prnt)
producer(filt)
Вывод:
1 2 4 8 16 32
Итак, то, что у нас есть, – это manufacturer(), действующий как источник, создающий некоторые значения, которые затем фильтруются перед печатью приемником, в данном случае сопрограммой printer().
my_filter (50, prnt) действует как единственный промежуточный шаг в конвейере и получает свою собственную сопрограмму в качестве аргумента.
Эта цепочка прекрасно иллюстрирует силу сопрограмм: они масштабируемы для более крупных проектов (все, что требуется, – это добавить больше этапов в конвейер) и легко обслуживаются (изменение одной не требует полной перезаписи исходного кода).
Сходства с объектами
Внимательный программист может заметить, что сопрограммы содержат определенное концептуальное сходство с объектами Python. От требуемого предварительного определения до объявления экземпляра и управления. Возникает очевидный вопрос: зачем использовать сопрограммы вместо испытанной парадигмы объектно-ориентированного программирования.
Что ж, помимо очевидного факта, что сопрограммам требуется только одно определение функции, они также выигрывают от того, что они значительно быстрее. Давайте рассмотрим следующий код:
class obj:
def __init__(self, value):
self.i = value
def send(self, num):
print(self.i + num)
inst = obj(1)
inst.send(5)
def coroutine(value):
i = value
while True:
num = (yield)
print(i + num)
cor = coroutine(1)
next(cor)
cor.send(5)
Вот как эти двое противостоят друг другу при запуске модуля timeit 10 000 раз:
| Объект | Сопрограмма |
|---|---|
| 0,791811 | 0,6343617 |
| 0,7997058 | 0,6383156 |
| 0,8579286 | 0,6365501 |
| 0,838439 | 0,648442 |
| 0,9604255 | 0,7242559 |
Оба выполняют одну и ту же черную задачу, но второй пример быстрее. Скорость возрастает из-за отсутствия самостоятельного поиска объекта.
Для задач, требующих большего количества системных налогов, эта функция дает веские причины использовать сопрограммы вместо обычных объектов-обработчиков.
Осторожно при использовании сопрограмм
Метод send() не является потокобезопасным:
import threading
from time import sleep
def print_number(cor):
while True:
cor.send(1)
def coroutine():
i = 1
while True:
num = (yield)
print(i)
sleep(3)
i += num
cor = coroutine()
next(cor)
t = threading.Thread(target=print_number, args=(cor,))
t.start()
while True:
cor.send(5)
Поскольку send() не был должным образом синхронизирован и не имеет встроенной защиты от ошибочных вызовов, связанных с потоками, возникла следующая ошибка ValueError: генератор уже выполняется.
Смешивать сопрограммы с параллелизмом следует с особой осторожностью.
Зацикливание сопрограмм невозможно:
def coroutine_1(value):
while True:
next_cor = (yield)
print(value)
value = value - 1
if next_cor != None:
next_cor.send(value)
def coroutine_2(next_cor):
while True:
value = (yield)
print(value)
value = value - 2
if next != None:
next_cor.send(value)
cor1 = coroutine_1(20)
next(cor1)
cor2 = coroutine_2(cor1)
next(cor2)
cor1.send(cor2)
То же ValueError показывает свое лицо. Из этих простых примеров мы можем сделать вывод, что метод send() создает своего рода стек вызовов, который не возвращается, пока цель не достигнет своего оператора yield.
Заключение
Сопрограммы представляют собой мощную альтернативу обычным механизмам обработки данных. Единицы кода можно легко комбинировать, изменять и переписывать, при этом пользуясь сохранением переменных на протяжении всего жизненного цикла.
В руках искусного программиста сопрограммы становятся значимыми новыми инструментами, упрощая проектирование и реализацию, при этом обеспечивая значительный прирост производительности.
Преобразование идей в простые процессы экономит силы и время программиста, избегая при этом набивать код лишними объектами, которые не выполняют ничего, кроме элементарных задач.
Автор