Рубрики
Без рубрики

Сопрограммы в Python

Сопрограммы – это особый тип функций, которые сознательно передают управление вызывающему объекту, но они не заканчивают свой контекст в процессе, поддерживая его в состоянии ожидания. В этой статье мы погрузимся в сопрограммы Python.

Автор оригинала: Dário Alves.

Сопрограммы в Python

Вступление

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

Когда программа вызывает функцию, ее текущий контекст выполнения сохраняется перед передачей управления функции и возобновлением выполнения. Затем функция создает новый контекст – с этого момента вновь созданные данные существуют исключительно во время выполнения функций.

Как только задача выполнена, управление передается обратно вызывающему – новый контекст эффективно удаляется и заменяется предыдущим.

Сопрограммы

Сопрограммы – это особый тип функции, которая намеренно передает управление вызывающему объекту, но не завершает его контекст в процессе, вместо этого поддерживая его в состоянии ожидания.

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

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

Генераторы производят данные

Сопрограммы потребляют данные

Четкая обработка ключевого слова yield определяет, манипулируем ли мы одним или другим.

Определение сопрограммы

Теперь, когда все самое необходимое осталось позади, давайте сразу же приступим к кодированию нашей первой сопрограммы:

def bare_bones():
    while True:
        value = (yield)

Очевидно, что вы видите сходство с обычной функцией Python. Блок while True: гарантирует непрерывное выполнение сопрограммы до тех пор, пока она получает значения.

Значение собирается с помощью оператора yield . Мы вернемся к этому через несколько минут…

Очевидно, что этот код практически бесполезен, поэтому мы завершим его несколькими операторами print :

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")

Затем наша переменная 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

Исключение стопитерации

После закрытия сопрограммы вызов 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

Итак, то , что мы имеем здесь, – это producer () , действующий как source , создающий некоторые значения, которые затем фильтруются перед печатью sink , в данном случае printer() coroutine.

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)

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

0.791811 0.634362
0.799706 0.638316
0.857929 0.636550
0.838439 0.648442
0.960426 0.724256

Оба выполняют одну и ту же черную работу, но второй пример быстрее. Увеличение скорости происходит из-за отсутствия поиска объекта self|/.

Для более трудоемких задач эта функция дает вескую причину использовать сопрограммы вместо обычных объектов-обработчиков.

Внимание при использовании сопрограмм

Метод 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 .

Таким образом, использование сопрограмм-это не только солнечный свет и радуга, но и тщательное обдумывание перед применением.

Вывод

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

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

Разбиение идей на простые процессы экономит усилия и время программиста, избегая при этом наполнения кода лишними объектами, которые выполняют не более чем элементарные задачи.