{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Генераторы\n", "\n", "## Выражение `yield`\n", "\n", "Если тело функции содержит хотя бы одно ключевое слово [yield](https://docs.python.org/3/reference/simple_stmts.html#yield), то это так называемая [функция-генератор](https://docs.python.org/3/glossary.html#term-generator). Вызов функции генератора не приводит к исполнению тела функции. Вместо этого функция-генератор возвращает [объект генератора](https://docs.python.org/3/glossary.html#term-generator-iterator)." ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "generator" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def generator_function():\n", " print(\"Начало функции.\")\n", " yield 0\n", "\n", "generator_object = generator_function()\n", "type(generator_object)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Объект генератора хранит в себе исходный код функции генератора, её пространство локальных переменных, а так же текущую точку выполнения. В этом и заключается главная разница между функцией и генератором. \n", "\n", "Функция не хранит своё состояние между своими вызовами, ключевое слово `return` возвращает управление вызывающему коду. Ключевое слово (выражение) `yield` в свою очередь, передаёт управление вызывающему коду, но запоминает место и состояние (значение локальных переменных и т.п.), а не сбрасывает его. Это позволяет в последствии возобновить исполнение кода в генераторе. \n", "\n", "Применение функции `next` к объекту генератора возобновляет (или начинает) исполнение кода в генерации до тех пор, пока не встретится выражение `yield`. Значение справа от `yield` возвращается функцией `next` вызывающему коду. Исполнение кода в генераторе ставится на паузу до следующего вызова функции `next`.\n", "\n", "```{note}\n", "Вообще говоря, у объекта генератора есть метод `send`, который позволяет не только получать объекты из генератора, но и передавать объекты из вызывающего кода в код генератора. Вызов этого метода тоже возобновляет исполнение генератора. Если `g` --- объект генератора, то выражение `g.send(None)` в точности эквивалентно выражению `next(g)`.\n", "```\n", "\n", "Продемонстрируем особенность объекта генератора на примере. Для этого напишем функцию-генератор обратного отсчета с 2." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "def countdown_from_2():\n", " print(f\"Начинаю обратный отсчет с двух.\")\n", " yield 2\n", " print(\"Продолжаю обратный отсчет. Следующее значение 1.\")\n", " yield 1\n", " print(\"Обратный отсчет закончен.\")\n", " return \n", "\n", "g = countdown_from_2()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выше объявлена функция-генератор `countdown_from_2`. И далее результата вызова этой функции связывается с именем `g`. Заметьте, что на экране не появилось сообщений, потому что на этот момент исполнение инструкций в тела функций ещё не началось: был создан только объект генератора.\n", "\n", "Применим функцию `next` к объекту генератора." ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Начинаю обратный отсчет с двух.\n", "Функция next вернула значение 2.\n" ] } ], "source": [ "print(f\"Функция next вернула значение {next(g)}.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы видим, что инструкции в теле цикла начали исполняться, но не до конца, а только до инструкции `yield 2`.\n", "```python\n", "def countdown_from_2():\n", " print(f\"Начинаю обратный отсчет с двух.\")\n", " yield 2 # <--- pause\n", " print(\"Продолжаю обратный отсчет. Следующее значение 1.\")\n", " yield 1\n", " print(\"Обратный отсчет закончен.\")\n", " return \n", "```\n", "\n", "Применим функцию `next` ещё раз." ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Продолжаю обратный отсчет. Следующее значение 1.\n", "Функция next вернула значение 1.\n" ] } ], "source": [ "print(f\"Функция next вернула значение {next(g)}.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Инструкции продолжились исполняться, пока не встретился очередной `yield`.\n", "\n", "```python\n", "def countdown_from_2():\n", " print(f\"Начинаю обратный отсчет с двух.\")\n", " yield 2 \n", " print(\"Продолжаю обратный отсчет. Следующее значение 1.\")\n", " yield 1 # <--- pause\n", " print(\"Обратный отсчет закончен.\")\n", " return \n", "```\n", "\n", "Последний раз применим функцию `next`. Заметим, что дальше по пути исполнения программы встречается ключевое слово `return`, а не `yield`.\n", "\n", "```python\n", "def countdown_from_2():\n", " print(f\"Начинаю обратный отсчет с двух.\")\n", " yield 2 \n", " print(\"Продолжаю обратный отсчет. Следующее значение 1.\")\n", " yield 1 \n", " print(\"Обратный отсчет закончен.\")\n", " return # <--- raise StopIteration\n", "```\n", "\n", "Ключевое слово `return` в генераторе возбуждает исключение `StopIteration`, что приводит к выходу из тела функции генератора." ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Обратный отсчет закончен.\n", "Генератор исчерпан.\n" ] } ], "source": [ "try:\n", " next(g)\n", "except StopIteration:\n", " print(\"Генератор исчерпан.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Итераторы vs генераторы\n", "\n", "Функции генераторы удобно задействовать для создания итераторов, т.к. они поддерживают тот же интерфейс: по запросу функции `next` выдаётся очередное значение, по исчерпании элементов возбуждается исключение `StopIteration`.\n", "\n", "В качестве примера рассмотрим наивную реализацию альтернативы `range`, но для действительных чисел. " ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 0.8999999999999999 0.9999999999999999 " ] } ], "source": [ "def frange(start, stop, step=1.0):\n", " while start < stop:\n", " yield start\n", " start += step\n", "\n", "for x in frange(0, 1, 0.1):\n", " print(x, end=\" \")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ключевое отличие генератора от классического итератора заключается в том, что итератор выдаёт уже существующие в каком-то контейнере значения, а генератор вычисляет новые значения на лету. Это позволяет экономить ресурсы системы, если для дальнейших вычислений не требуются, чтобы все значения где-то хранились в одном месте.\n", "\n", "В качестве примера рассмотрим вычисление числа $\\pi$ через сумму ряда \n", "\n", "$$\n", "\\sum_{n=1}^\\infty \\dfrac{1}{n^2} = \\dfrac{\\pi^2}{6}.\n", "$$" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.141591698659554 3.141591698659554\n" ] } ], "source": [ "from math import sqrt\n", "\n", "def list_of_terms(N):\n", " return [1./(n * n) for n in range(1, N)]\n", "\n", "\n", "def generator_of_terms(N):\n", " for n in range(1, N):\n", " yield 1/(n * n)\n", "\n", "\n", "def pi_from_sum(S):\n", " return sqrt(6*S)\n", "\n", "\n", "N = 1_000_000\n", "S1 = sum(list_of_terms(N))\n", "S2 = sum(generator_of_terms(N))\n", "\n", "print(pi_from_sum(S1), pi_from_sum(S2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Профилирование по памяти продемонстрирует, что функция с генератором гораздо экономнее при больших `N`, чем функция со списком. Это объясняется тем, что ни в один момент времени не создаётся список, чтобы хранить члены ряда. Вместо этого, они вычисляются по запросу функции `sum`. Такой подход, когда вычисления откладываются до тех пор, пока не потребуется их результат, называют [ленивыми вычислениями](https://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D0%BD%D0%B8%D0%B2%D1%8B%D0%B5_%D0%B2%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F).\n", "\n", "Т.к. генератор поддерживает протокол итерации, то при необходимости можно получить список из генератора. " ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]\n" ] } ], "source": [ "print(list(frange(0, 1, 0.1)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Но это может привести к зацикливанию, если генератор никогда не исчерпается. В этом заключается ещё одно крупное отличие генераторов от итераторов: генераторы могут генерировать бесконечную последовательность элементов, а итераторы всегда пробегаются по расположенной в памяти, а значит конечной, последовательности элементов. Например, следующий генератор выдаёт бесконечную последовательность натуральных чисел.\n", "\n", "```{note}\n", "В модуле [itertools](https://docs.python.org/3/library/itertools.html) реализован генератор [count](https://docs.python.org/3/library/itertools.html#itertools.count), с помощью которого можно добиться точно такого же поведения.\n", "```" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [], "source": [ "def count():\n", " x = 1\n", " while True:\n", " yield x\n", " x += 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Т.к. такой генератор никогда не исчерпается, то он никогда и не бросит исключения `StopIteration`, а значит применять его в цикле `for` можно только в случае, если предусмотрен выход оператором `break`. Иначе программа зациклится. \n", "\n", "## Генераторные выражения\n", "\n", "Если вместо квадратных скобочек в списковом включении указать круглые, то вы получите [генераторное выражение](https://docs.python.org/3/glossary.html#term-generator-expression). Разница заключается в том, что в случае спискового включения все вычисления производятся сразу и в результате выходит список, а в случае генераторного выражения вы получаете объект генератора, что само по себе не приводит к никаким вычислениям.\n", "\n", "Чтобы продемонстрировать это, создадим список и генератор схожим выражением и измерим, сколько байт занимает каждый из них.\n", "\n", "```{note}\n", "Метод [getsizeof](https://docs.python.org/3/library/sys.html#sys.getsizeof) измеряет количество байт, которое занимает объект в памяти. Он корректно работает для всех встроенных объектов и объектов из стандартной библиотеки, но может давать ложную информацию для пользовательских объектов и объектов из сторонних библиотек.\n", "```" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Список занимает 8697456 байт\n", "Генератор занимает 112 байт\n" ] } ], "source": [ "from sys import getsizeof\n", "\n", "l = [x * x for x in range(1_000_000)]\n", "g = (x * x for x in range(1_000_000))\n", "\n", "print(f\"Список занимает {getsizeof(l)} байт\")\n", "print(f\"Генератор занимает {getsizeof(g)} байт\")" ] } ], "metadata": { "interpreter": { "hash": "cd49b4596bae9c980ff74fdf93e8fe80e447435ae307c062fad6c4f9ef2eb47f" }, "kernelspec": { "display_name": "Python 3.8.10 ('venv': venv)", "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.8.10" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }