<jupyter_start><jupyter_text>Генераторы и сопрограммы (coroutine)* Как устроены сопрограммы* Отличие между генераторами и сопрограммами* Как работает `yield from`* Примеры работы сопрограмм<jupyter_code>def grep(pattern):
    print('start grep')
    
    while True:
        # Переменной line присваивается результат работы yield. Функция grep как бы заморозит
        # свое значение и будет ожидать ввода данных при помощи метода send()
        # line будет равна значению переданному методом send()
        line = yield

        if pattern in line:
            print(line)


# Вызывая функцию grep() мы создаем (не вызываем!) coroutine или
# по другому generator, по которому следует итерироваться:
g = grep('python') # <class 'generator'>

# Для того, чтобы запустить coroutine, необходимо вызвать метод next(),
# после чего код функции запустится и на экран выведется строчка "start grep",
# запустится бесконечный цикл while, код дойдет до инструкции yield, и здесь вернется
# управление в основной поток.
next(g) # g.send(None)

# После того, как управление перешло в основной поток, мы отправляем данные
# coroutine и код возобновляет свою работу дальше. В переменную line запишется
# значение переданное методом send(), а параметр pattern возобновит свое значение
# которое было присвоено при создании coroutine.
g.send('golang is better?')
g.send('python is simple!')<jupyter_output>start grep
python is simple!<jupyter_text>Повторим:Инструкция `yield` возвращает результат и замораживает функцию. Этот возврат не равносилен инструкции `return`. В объекте `g` который является `coroutine` запомнился указатель на фрэйм стека, запомнились все локальные переменные. Функция как бы запомнила свое состояние, как бы заморозила свое выполнение в том месте, где была указана инструкция `yield`. С этого момента запущена `coroutine` которая ожидает вызова метода `send()` с переданными в неё данными.   Coroutine. Метод `close()`Иногда, необходимо закрыть запущенную `coroutine`. Выполняется это при помощи метода `close()` для объекта `coroutine`. Метод `close()` сгенерирует исключение `GeneratorExit` в том месте, где функция заморозила свою работу. Это исключение нельзя игнорировать, его нужно обрабатывать и корректно завершать работу `coroutine`.<jupyter_code>def grep(pattern):
    print('start grep')
    try:
        while True:
            line = yield
            if pattern in line:
                print(line)
    except GeneratorExit:
        print('stop grep') # Вызывается каждый раз, когда доходит до инструкции yield в теле coroutine


# Функция grep при вызове не исполняется привычным образом,
# python видя инстуркцию yield создает итерируемый объект-генератор который ожидает итерацию
g = grep('python') # <generator object grep at 0x7ff2fc2b5d00>
next(g)            # Аналогично - g.send(None). Первой итерацией запускается coroutine
g.send('python is the best!')
g.close()          # Метод вызывает исключение типа GeneratorExit в coroutine<jupyter_output>start grep
python is the best!
stop grep<jupyter_text>Coroutine. Метод `throw()`Иногда необходимо передать исключение в `coroutine`. Это выполняется при помощи вызова метода `throw()`:<jupyter_code>def grep(pattern):
    print('start grep')
    try:
        while True:
            line = yield
            if pattern in line:
                print(line)
    except GeneratorExit:
        print('stop grep') # Вызывается каждый раз, когда доходит до инструкции yield в теле coroutine
    except RuntimeError as e:
        print('Custom exception. e =', e)


# Функция grep при вызове не исполняется привычным образом,
# python видя инстуркцию yield создает итерируемый объект-генератор который ожидает итерацию
g = grep('python') # <generator object grep at 0x7ff2fc2b5308>
next(g)            # Аналогично - g.send(None). Первой итерацией запускается coroutine
g.send('python is the best!')
g.throw(RuntimeError, 'something wrong')
# g.close()<jupyter_output>start grep
python is the best!
Custom exception. e = something wrong<jupyter_text>Coroutine. Вызов `coroutine` из обычной функции<jupyter_code>def grep(pattern):
    print('start grep')
    while True:
        line = yield
        if pattern in line:
            print(line)

def grep_python_coroutine():
    g = grep('python') # Создается coroutine
    next(g)            # Аналогично - g.send(None). Первой итерацией запускается coroutine
    g.send('python is the best!')
    g.close()


# Функция grep_python_coroutine() не является coroutine, в ее теле нет инструкции yield,
# поэтому она сразу выполнится, так как является обычной функцией:
print(type(grep_python_coroutine))
grep_python_coroutine()<jupyter_output><class 'function'>
start grep
python is the best!<jupyter_text>Coroutine. Инструкция `yield from`Для того, чтобы из одной `coroutine` можно было вызывать другую `coroutine`, в Python используется инструкция `yield from`:<jupyter_code>def grep(pattern):
    print('start grep')
    while True:
        line = yield
        if pattern in line:
            print(line)

def grep_python_coroutine():
    g = grep('python')
    yield from g


# Сейчас функция grep_python_coroutine() является coroutine, так как содержит
# инструкцию yield from вызывающую другую coroutine
g = grep_python_coroutine()
print(g)
next(g)   # g.send(None)
g.send('python wow!')<jupyter_output><generator object grep_python_coroutine at 0x7f8c18643af0>
start grep
python wow!<jupyter_text>PEP 380. Генераторы<jupyter_code>def chain(x_iterable, y_iterable):
    # В инструкции 'yield from' указываем объект по которому возможна итерация,
    # в этом случае этими объектами являются параметры переданные в эту функцию.
    # В начале функция-генератор chain() возвращает элементы по первой инструкции 'yield from x_iterable',
    # как только элементы закончаться, вызов перейдет ко второй инструкции 'yield from y_iterable'
    yield from x_iterable
    yield from y_iterable

    # По сути, это тоже самое, если мы перепишем функцию:
    #
    # def chain(x_iterable, y_iterable):
    #     for x in x_iterable:
    #         yield x
    #     for y in y_iterable:
    #         yield y


# Два объекта по которым возможно осуществлять итерацию, это list() и tuple()
a = [1, 2, 3]
b = (4, 5)

for x in chain(a, b): # Итерация сначала идет по объекту 'a', а затем по 'b'
    print(x)<jupyter_output>1
2
3
4
5
{'fraction_non_alphanumeric': 0.07817178881008668, 'fraction_numerical': 0.0058313632781717886, 'mean_word_length': 4.0325138778747025, 'pattern_counts': {'":': 0, '<': 24, '<?xml version=': 0, '>': 24, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '158269', 'license': 'MIT', 'n_tokens_mistral': 2312, 'n_tokens_neox': 2591, 'n_words': 696, 'path': '5/5.3.3_generators_and_coroutines.ipynb', 'repo_name': 'microcoder/course-python-mipt', 'size': '6345'}