{ "cells": [ { "cell_type": "markdown", "id": "888a9140", "metadata": {}, "source": [ "# Генераторы и сопрограммы #" ] }, { "cell_type": "markdown", "id": "fd782031", "metadata": {}, "source": [ "На предыдущей лекции мы рассмотрели то, как работают генераторы в Python. На этой лекции мы познакомимся с сопрограммами и выясним, в чем отличие и сходство между генераторами и сопрограммами в Python. Также мы обсудим, как работает конструкция `yield from` и рассмотрим примеры работы различных сопрограмм.\n", "\n", "Итак, давайте перейдем к примеру и рассмотрим, как выглядит сопрограмма." ] }, { "cell_type": "code", "execution_count": 1, "id": "5ac63f59", "metadata": {}, "outputs": [], "source": [ "# Сопрограммы (корутины)\n", "def grep(pattern):\n", " print(\"start grep\")\n", " while True:\n", " line = yield\n", " \n", " if pattern in line:\n", " print(line)" ] }, { "cell_type": "code", "execution_count": 2, "id": "ba7628fb", "metadata": {}, "outputs": [], "source": [ "g = grep(\"python\")" ] }, { "cell_type": "code", "execution_count": 3, "id": "cb95ffaa", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "start grep\n" ] } ], "source": [ "next(g)" ] }, { "cell_type": "code", "execution_count": 4, "id": "2ba2e231", "metadata": {}, "outputs": [], "source": [ "g.send(\"golang is better?\")" ] }, { "cell_type": "code", "execution_count": 5, "id": "ced3ecae", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "python is simple!\n" ] } ], "source": [ "g.send(\"python is simple!\")" ] }, { "cell_type": "markdown", "id": "0bcba4fa", "metadata": {}, "source": [ "Предположим, наша цель — фильтровать входной поток при помощи функции `grep`. В функцию `grep` мы передаем некий паттерн, например, это пусть будет строчка `python`. Далее мы должны в эту функцию передать некие строчки и вывести на экран только те, в которых присутствует строка `python`. При помощи сопрограмм наша функция `grep` выглядит следующим образом.\n", "\n", "Мы в бесконечном цикле вызываем такую конструкцию - `line` присвоить `yield`. В этом месте очень важно уделить внимание отличию от того, как `yield` используется в генераторах. В генераторах мы выражение присваивали справа от конструкции `yield`, а здесь мы, наоборот, переменной присваиваем результат работы конструкции `yield`.\n", "\n", "Что произойдет в данном случае? В данном случае функция как бы заморозит свое значение и будет ожидать ввода данных при помощи метода `send`. Итак, если мы вызовем функцию `grep`, будет создана корутина. Для того чтобы запустить нашу корутину, необходимо вызвать метод `next`. После того, как метод `next` будет вызван, запустится код нашей функции, выведется строчка `start grep`, запустится бесконечный цикл, код дойдет до инструкции `yield`, и здесь вернется управление в основной поток.\n", "\n", "После этого в основном потоке мы отправляем данные нашей корутине, и код возобновляет свою работу дальше. Давайте посмотрим, как код будет выполняться в отладчике в консоли. Нам потребуется код нашего примера этой утилиты — это наша корутина `grep`. Запускаем отладчик при помощи команды python3, указываем флаг `−m pdb` и запускаем наш отладчик." ] }, { "cell_type": "code", "execution_count": null, "id": "d2fa9a73", "metadata": {}, "outputs": [], "source": [ "! python -m pdb ex4.py" ] }, { "cell_type": "markdown", "id": "5cb7ca6c", "metadata": {}, "source": [ "Давайте разбираться с тем, как будет выполняться наша корутина. Итак, мы объявили нашу функцию `grep`. Далее, мы создали объект типа генератор. Давайте еще раз убедимся, что это действительно генератор. Наш объект `g`, после того как ему присвоили выражение `grep(\"python\")`, создался, а вызова пока еще не произошло никакого для этой функции. Далее мы вызываем метод `next(g)`, и это будет равносильно тому, что мы бы вызвали метод `g.send` и туда передали значение `None` и эта конструкция запустит нашу корутину.\n", "\n", "Итак, мы провалились в нашу функцию, вывели в консоль строчку \"start grep\", зашли в наш цикл, дошли до инструкции `yield`, и здесь произошел возврат. Хочу обратить ваше внимание, как произошел этот возврат. Он не равносилен инструкции `return` для функции. Дело в том, что в объекте `g`, который является корутиной, запомнился указатель на фрейм стека, запомнились все локальные переменные. Функция помнит свое состояние. Она как бы заморозила свое выполнение в том месте, где была указана инструкция `yield`. Теперь наша корутина запущена, она помнит свое состояние, и она ожидает, пока кто-нибудь вызовет метод `send` и отправит в нее данные. Давайте продолжим выполнение. Итак, мы отправили данные при помощи вызова `send`. Мы опять вернулись в нашу функцию, но вернулись именно в то место, где завершили или заморозили свою работу. Это инструкция `yield`. Инструкций `yield` может быть несколько. Давайте посмотрим, чему равна переменная `line`. В данном случае наше условие для фильтрации не будет выполнено, и на экран ничего не напечатается. Давайте продолжим выполнение. Мы снова в цикле продолжаем работу этой нашей корутины, снова выполняем строчку `line` присвоить `yield`, возвращаемся из нашей корутины. В основном потоке мы опять отправляем данные в нашу корутину. В данном случае это будет строчка \"python is simple\". Опять проваливаемся в нашу функцию. Давайте посмотрим, чему равна переменная `line`. Да, действительно, это то, что мы в нее отправили при помощи метода `send`. В данном случае наша корутина напечатает на экран эту строчку. Все, я сейчас запустил все заново. На самом деле у функции `g` наша программа прекратила свою работу.\n", "\n", "Итак, давайте еще раз остановимся на ключевых моментах и отличии того, как работает корутина по сравнению с генератором. Здесь мы видим немного другое использование инструкции `yield`. Мы в переменную присваиваем ее значение. Внутри корутины она не производит значение, она потребляет значение, и точно так же приостанавливает или замораживает свое состояние, ждет, пока это значение в нее поступит. После поступления значения она возобновляет свою работу. Давайте продолжим изучать особенности корутин и посмотрим следующий пример." ] }, { "cell_type": "code", "execution_count": 6, "id": "8a676c5e", "metadata": {}, "outputs": [], "source": [ "# Сопрограммы, вызов метода close()\n", "def grep(pattern):\n", " print(\"start grep\")\n", " try:\n", " while True:\n", " line = yield\n", " if pattern in line:\n", " print(line)\n", " \n", " except GeneratorExit:\n", " print(\"stop grep\")" ] }, { "cell_type": "code", "execution_count": 7, "id": "0b51a749", "metadata": {}, "outputs": [], "source": [ "g = grep(\"python\")" ] }, { "cell_type": "code", "execution_count": 8, "id": "c5133a0a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "start grep\n" ] } ], "source": [ "next(g)" ] }, { "cell_type": "code", "execution_count": 9, "id": "0a2d7d0a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "python is the best!\n" ] } ], "source": [ "g.send(\"python is the best!\")" ] }, { "cell_type": "code", "execution_count": 10, "id": "7b508e1f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "stop grep\n" ] } ], "source": [ "g.close()" ] }, { "cell_type": "markdown", "id": "fde51a2c", "metadata": {}, "source": [ "Иногда необходимо остановить запущенную корутину. Делается это при помощи вызова метода `close` для объекта корутины. Метод `close` будет вызван автоматически сборщиком мусора, но если нам нужно самим остановить корутину, то можно руками вызвать метод `close`. Метод `close` сгенерирует исключение генератора `exit` в том месте, где функция заморозила свое значение, свою работу.\n", "\n", "Это исключение нельзя игнорировать, его нужно обрабатывать и завершать свою работу. Обрабатывать его можно при помощи стандартной работы с исключениями, при помощи блока `try except`.\n", "\n", "Иногда необходимо передать исключения в саму корутину. Это делается при помощи вызова метода `throw`. Пример точно такой же." ] }, { "cell_type": "code", "execution_count": 11, "id": "7f4a00f5", "metadata": {}, "outputs": [], "source": [ "# Сопрограммы, генерация исключений\n", "def grep(pattern):\n", " print(\"start grep\")\n", " try:\n", " while True:\n", " line = yield\n", " if pattern in line:\n", " print(line)\n", " \n", " except GeneratorExit:\n", " print(\"stop grep\")" ] }, { "cell_type": "code", "execution_count": 12, "id": "84247ff8", "metadata": {}, "outputs": [], "source": [ "g = grep(\"python\")" ] }, { "cell_type": "code", "execution_count": 13, "id": "05476865", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "start grep\n" ] } ], "source": [ "next(g)" ] }, { "cell_type": "code", "execution_count": 14, "id": "17031b8a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "python is the best!\n" ] } ], "source": [ "g.send(\"python is the best!\")" ] }, { "cell_type": "code", "execution_count": 15, "id": "bfabf7a3", "metadata": { "scrolled": true }, "outputs": [ { "ename": "RuntimeError", "evalue": "something wrong", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mthrow\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mRuntimeError\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"something wrong\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mgrep\u001b[0;34m(pattern)\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mline\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32myield\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpattern\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mline\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mline\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mRuntimeError\u001b[0m: something wrong" ] } ], "source": [ "g.throw(RuntimeError, \"something wrong\")" ] }, { "cell_type": "markdown", "id": "59273372", "metadata": {}, "source": [ "Наша корутина приостановила свою работу на вот этой инструкции, и если мы отправим в нее исключение, то оно будет сгенерировано именно в этом месте. Сгенерированное исключение можно обработать при помощи блока `try except`, в общем, стандартным способом для языка Python.\n", "\n", "Давайте посмотрим еще более сложный пример." ] }, { "cell_type": "code", "execution_count": 16, "id": "20a857c8", "metadata": {}, "outputs": [], "source": [ "# Вызовы сопрограмм, PEP 380\n", "def grep(pattern):\n", " print(\"start grep\")\n", " while True:\n", " line = yield\n", " if pattern in line:\n", " print(line)\n", " \n", "def grep_python_coroutine():\n", " g = grep(\"python\")\n", " next(g)\n", " g.send(\"python is the best!\")\n", " g.close()" ] }, { "cell_type": "code", "execution_count": 17, "id": "3bb19540", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "start grep\n", "python is the best!\n" ] } ], "source": [ "g = grep_python_coroutine() # is g coroutine?" ] }, { "cell_type": "code", "execution_count": 18, "id": "8938caa3", "metadata": {}, "outputs": [], "source": [ "g" ] }, { "cell_type": "markdown", "id": "75a1805d", "metadata": {}, "source": [ "У нас есть функция `grep`, У нас есть корутина `grep`, у нее внутри есть инструкция `yield`, и мы хотим вызвать нашу корутину `grep` в другой корутине. Как это сделать? \n", "\n", "Если мы напишем код, который приведен на слайде, и потом сделаем данный вызов, то будет ли переменная `g` являться корутиной? На самом деле, не будет. Так как она не содержит инструкции `yield`, она выполнится сразу, она будет являться обычной функцией. Для того чтобы было удобно вызывать из одних корутин другие, в Python разработали стандарт `PEP 0380` и в Python 3 его реализовали. И теперь в Python 3 появилась инструкция `yield from`. При помощи нее можно выполнить делегирование вызова одной корутины в другой. Наш пример можно переписать следующим образом." ] }, { "cell_type": "code", "execution_count": 19, "id": "f9c1c496", "metadata": {}, "outputs": [], "source": [ "# Сопрограммы, yield from PEP 0380\n", "def grep(pattern):\n", " print(\"start grep\")\n", " while True:\n", " line = yield\n", " if pattern in line:\n", " print(line)\n", " \n", "def grep_python_coroutine():\n", " g = grep(\"python\")\n", " yield from g" ] }, { "cell_type": "code", "execution_count": 20, "id": "6e7ce4c8", "metadata": {}, "outputs": [], "source": [ "g = grep_python_coroutine() # is g coroutine?" ] }, { "cell_type": "code", "execution_count": 21, "id": "6c4e848e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g" ] }, { "cell_type": "code", "execution_count": 22, "id": "9c818942", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "start grep\n" ] } ], "source": [ "g.send(None)" ] }, { "cell_type": "code", "execution_count": 23, "id": "d5052d2c", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "python wow!\n" ] } ], "source": [ "g.send(\"python wow!\")" ] }, { "cell_type": "markdown", "id": "ff9ded26", "metadata": {}, "source": [ "То есть, мы используем инструкцию `yield from` и указываем объект в виде другой корутины. Теперь, если мы попробуем вызвать нашу функцию `grep_python_coroutine`, она будет являться корутиной или генератором, и для того, чтобы продолжить и выполнить код, который находится у нее внутри, необходимо вызвать метод `send`, передать туда значение `none` и далее вызвать метод `send` и передать нужную строчку. Таким образом, в Python стало очень легко и удобно вызывать из одной корутины другую.\n", "\n", "Для обычных генераторов инструкцию `yield` можно использовать как замену цикла `for`, внутри которого вызывается инструкция `yield`. Рассмотрим пример." ] }, { "cell_type": "code", "execution_count": 24, "id": "ffe0106e", "metadata": {}, "outputs": [], "source": [ "# PEP 380, генераторы\n", "def chain(x_iterable, y_iterable):\n", " yield from x_iterable\n", " yield from y_iterable" ] }, { "cell_type": "code", "execution_count": 25, "id": "47f4f8b9", "metadata": {}, "outputs": [], "source": [ "a = [1, 2, 3]" ] }, { "cell_type": "code", "execution_count": 26, "id": "00dc65e5", "metadata": {}, "outputs": [], "source": [ "b = (4, 5)" ] }, { "cell_type": "code", "execution_count": 27, "id": "f92a3508", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n", "4\n", "5\n" ] } ], "source": [ "for x in chain(a, b):\n", " print(x)" ] }, { "cell_type": "markdown", "id": "21998335", "metadata": {}, "source": [ "Например, у нас есть два объекта, по которым возможно осуществлять итерацию. Это список `a = [1, 2, 3]` и `tuple`, который мы запоминаем в переменную `b = (4, 5)`. Далее, мы в цикле вызываем функцию `chain`, передаем туда эти списки и выводим то, что нам генерирует наша функция `chain`. В функции `chain` мы используем конструкцию `yield from` и передаем туда объект, по которому возможна итерация. По сути, это то же самое, если мы функцию перепишем в таком виде" ] }, { "cell_type": "code", "execution_count": null, "id": "7a82b907", "metadata": {}, "outputs": [], "source": [ "def the_same_chain(x_iterable, y_iterable):\n", " for x in x_iterable:\n", " yield x\n", " \n", " for y in y_iterable:\n", " yield y" ] }, { "cell_type": "markdown", "id": "f8ff665d", "metadata": {}, "source": [ "то `yield from` будет работать точно так же, как цикл `for`, внутри которого вызвана инструкция `yield`." ] }, { "cell_type": "markdown", "id": "c754a0cd", "metadata": {}, "source": [ "Итак, на этой лекции мы рассмотрели то, как устроены корутины в Python, а также рассмотрели их сходство с генераторами и различия.\n", "\n", "Еще раз хочу обратить ваше внимание на то, что генераторы производят значение, а корутины потребляют значение. Корутина может замораживать свое состояние, и восстанавливается это состояние, после того как в нее отправили данные при помощи вызова метода `send`. При помощи вызова `close` можно завершать выполнение корутины, передавать исключения в нее. Также в Python 3 можно использовать конструкцию `yield from` для вызова одной корутины в другой. Все эти знания потребуются нам для того, чтобы понять, как устроен фреймворк `asyncio` и в следующих лекциях мы попробуем разобраться с устройством этого фреймворка." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }