{ "cells": [ { "cell_type": "markdown", "id": "748ad676", "metadata": {}, "source": [ "# Генераторы #" ] }, { "cell_type": "markdown", "id": "81ac59b4", "metadata": {}, "source": [ "На этой лекции мы с вами обсудим генераторы, которые являются очень важным механизмом, на котором построено многое в Python'е.\n", "\n", "Итак, простейший генератор — это функция, в которой есть оператор `yield`." ] }, { "cell_type": "code", "execution_count": 1, "id": "20b39e22", "metadata": {}, "outputs": [], "source": [ "def even_range(start, end):\n", " current = start\n", " while current < end:\n", " yield current\n", " current += 2" ] }, { "cell_type": "markdown", "id": "8dd3b8b1", "metadata": {}, "source": [ "Что же делает наш генератор? У нас генератор — это `even_range`, который работает по упрощенной схеме знакомого вам генератора `range`. Он принимает `(start, end)`, записывает текущий `start`, и пока у нас `current` меньше конечного значения, он `yield`'ит `current` и прибавляет двойку." ] }, { "cell_type": "code", "execution_count": 2, "id": "c4a176d4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "2\n", "4\n", "6\n", "8\n" ] } ], "source": [ "for number in even_range(0, 10):\n", " print(number)" ] }, { "cell_type": "markdown", "id": "94a8da7f", "metadata": {}, "source": [ "Что же делает этот оператор `yield`? `Yield` можно рассматривать как какой-то временный `return`, то есть у нас возвращается значение `current`. Однако выполнение функции не прекращается, это не обычный `return`, `return`, как вы видите, здесь вот в конце, у нас `return None` по умолчанию. `Yield` возвращает значение, но прерывает выполнение функции только на время, то есть мы можем вернуться к этой функции, к этому моменту.\n", "\n", "Важно знать, что мы можем итерироваться, например, по генератору, можем пробежаться и вывести все значения." ] }, { "cell_type": "markdown", "id": "f2e7ddcd", "metadata": {}, "source": [ "Каждый раз, когда у нас выполняется `yield`, у нас возвращается значение `current`, и каждый раз, когда мы просим следующий элемент, у нас выполнение функции возвращается в этот же момент, и мы идем дальше. Чтобы посмотреть, как это происходит на самом деле, можно воспользоваться функцией next, которая действительно применяется каждый раз при итерации. Таким образом, мы создаем наш итератор, наш генератор, и вызываем `next`." ] }, { "cell_type": "code", "execution_count": 3, "id": "f652e038", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ranger = even_range(0, 4)\n", "next(ranger)" ] }, { "cell_type": "code", "execution_count": 4, "id": "78b9cbcb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "next(ranger)" ] }, { "cell_type": "code", "execution_count": 5, "id": "30534428", "metadata": {}, "outputs": [ { "ename": "StopIteration", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mranger\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m: " ] } ], "source": [ "next(ranger)" ] }, { "cell_type": "markdown", "id": "5d8c18d1", "metadata": {}, "source": [ "В начале у нас получается 0. Потом вызывается `next`, получается 2. И когда у нас наш генератор исчерпан, когда у нас условие, что текущее значение меньше 4 не выполняется, — у нас вызывается `StopIteration`, выбрасывается исключение и больше `yield`'ов нет, у нас происходит `return`.\n", "\n", "Именно так работают генераторы. Чтобы понять, что действительно происходит, можно, например, добавить сюда `print` после `yield`'а." ] }, { "cell_type": "code", "execution_count": 9, "id": "c7a44318", "metadata": {}, "outputs": [], "source": [ "def list_generator(list_obj):\n", " for item in list_obj:\n", " yield item\n", " print(f\"After yielding {item}\")\n", " \n", "generator = list_generator([1, 2])" ] }, { "cell_type": "markdown", "id": "3ee7ee21", "metadata": {}, "source": [ "Давайте посмотрим, что происходит при вызове `next`." ] }, { "cell_type": "code", "execution_count": 7, "id": "441b6575", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "next(generator)" ] }, { "cell_type": "markdown", "id": "b405a97b", "metadata": {}, "source": [ "У нас есть наш `list_generator`, он принимает `list` какой-то `object` и просто `yield`'ит все элементы по порядку. Мы вызываем `next(generator)`, то есть берем следующий элемент из генератора, у нас возвращается единичка, первый элемент списка. Логично. Однако у нас почему-то не вывелось `print`. Не вывелось именно потому, что у нас выполнение функции генератора прервано. На этом моменте у нас вернулся элемент, и у нас запомнилось состояние функции в какой-то этот момент. " ] }, { "cell_type": "code", "execution_count": 8, "id": "870d8e66", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "After yielding 1\n" ] }, { "data": { "text/plain": [ "2" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "next(generator)" ] }, { "cell_type": "markdown", "id": "64be79b0", "metadata": {}, "source": [ "И выводится `After yielding` у нас выводится, когда мы берем следующий элемент, то есть у нас выполнение продолжается с вот этого момента и идет до тех пор, пока нет следующего `yield`'а. Таким образом мы доходим до двойки и все." ] }, { "cell_type": "code", "execution_count": 12, "id": "420efed7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "After yielding 2\n" ] }, { "ename": "StopIteration", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mgenerator\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m: " ] } ], "source": [ "next(generator)" ] }, { "cell_type": "markdown", "id": "cab954f3", "metadata": {}, "source": [ "Собственно, чем полезны эти генераторы? Казалось бы, концепт понятный, но не понятно, зачем их использовать. Оказывается, очень важной бывает именно вот эта вот особенность, которая позволяет хранить состояние функции и возвращаться к этому состоянию раз за разом. Таким образом, например, реализована функция `range`. Функция `range`, как вы знаете, позволяет вам получить генератор, какой-то итерабельный объект, по которому мы можем пробежаться и, например, выводить просто в цикле числа или делать что-нибудь более сложное. Функция `range` позволяет вам не загружать в память сразу огромный список чисел. Таким образом раньше в Python'е, например, была функция `range` безгенераторная и `xrange` генераторная. Функция `range` загружала в память огромное количество чисел, чтобы потом по ним итерироваться. Генератор позволяет вам делать очень просто. Он позволяет запомнить текущее положение и от него уже идти дальше." ] }, { "cell_type": "markdown", "id": "f534d54f", "metadata": {}, "source": [ "Классический пример — это числа Фибоначчи." ] }, { "cell_type": "code", "execution_count": 13, "id": "ff283572", "metadata": {}, "outputs": [], "source": [ "def fibonacci(number):\n", " a = b = 1\n", " \n", " for _ in range(number):\n", " yield a\n", " a, b = b, a + b" ] }, { "cell_type": "markdown", "id": "d4edb25a", "metadata": {}, "source": [ "Нам нужно получить числа Фибоначчи до какого-то момента. Опять же, обратите внимание, нам не нужно запоминать огромное количество чисел Фибоначчи, которые очень быстро растут. Нам нужно здесь только помнить два конкретных числа и возвращаться в момент, когда мы остановились." ] }, { "cell_type": "code", "execution_count": 14, "id": "ca0e7f6b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "1\n", "2\n", "3\n", "5\n", "8\n", "13\n", "21\n", "34\n", "55\n" ] } ], "source": [ "for num in fibonacci(10):\n", " print(num)" ] }, { "cell_type": "markdown", "id": "8af9fb69", "metadata": {}, "source": [ "Итак, очень важная особенность генераторов, что они позволяют хранить состояние и возвращаться к нему и использовать это для того, чтобы оптимизировать работу с памятью. " ] }, { "cell_type": "markdown", "id": "c9054bfd", "metadata": {}, "source": [ "Еще один очень важный момент — это возможность генераторам получать какие-то значения. Мы можем не только возвращаться к моменту, когда у нас генератор сделал `yield`, и продолжать исполнение с этого момента с каким-то контекстом, мы можем еще и передавать туда значения. Этот момент используется очень активно в асинхронном программировании, о котором мы с вами поговорим чуть позже." ] }, { "cell_type": "markdown", "id": "f33f1bd3", "metadata": {}, "source": [ "А пока давайте определим `accumulator`, нашу генераторную функцию, которая хранит какое-то общее количество данных и просто в бесконечном цикле получает с помощью оператора `yield` значение. Давайте посмотрим, что здесь происходит в простейшем виде." ] }, { "cell_type": "code", "execution_count": 16, "id": "97d62f6d", "metadata": {}, "outputs": [], "source": [ "def accumulator():\n", " total = 0\n", " while True:\n", " value = yield total\n", " print(f\"Got: {value}\")\n", " \n", " if not value: break\n", " \n", " total += value\n", " \n", "generator = accumulator()" ] }, { "cell_type": "code", "execution_count": 17, "id": "161da2c5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "next(generator)" ] }, { "cell_type": "markdown", "id": "55ec02a9", "metadata": {}, "source": [ "Вначале мы инициализируем наш генератор, то есть мы его стартуем. У нас возвращается `yield total`, которая равна нулю по умолчанию в начале цикла. Дальше, мы можем послать данные в генератор с помощью метода генератора `send`." ] }, { "cell_type": "code", "execution_count": 18, "id": "e359097d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Got: 1\n", "Accumulated: 1\n" ] } ], "source": [ "print(f\"Accumulated: {generator.send(1)}\")" ] }, { "cell_type": "markdown", "id": "c6c8b6c3", "metadata": {}, "source": [ "Что же здесь мы делаем? Мы передаем в наш генератор единичку, мы посылаем туда значение, то есть у нас есть какая-то точка исполнения, у нас остановилось исполнение здесь. Мы можем послать в эту точку значение, и это значение запишется в `value`. Таким образом мы передали в `value` единицу. Мы можем вывести, что мы действительно получили единицу.\n", "\n", "Дальше, если у нас в `value` не было передано, мы хотим выйти. У нас есть `value`, поэтому мы прибавляем к нашему `total`'у `value`, которому мы передали. Собственно, accumulator аккумулирует значение, что логично. Итак, у нас накопилась единичка.\n", "\n", "Дальше, мы можем послать еще одну единичку и окажется, что мы действительно получили единичку, а накопили уже два, потому что наша функция хранит состояние, и у нас хранится контекст, в котором у нас `total` уже равен двум." ] }, { "cell_type": "code", "execution_count": 19, "id": "9d27bdf5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Got: 1\n", "Accumulated: 2\n" ] } ], "source": [ "print(f\"Accumulated: {generator.send(1)}\")" ] }, { "cell_type": "markdown", "id": "0b4adf6b", "metadata": {}, "source": [ "Мы можем передать еще одну единичку, и у нас очевидно `Accumulated` станет равен трем." ] }, { "cell_type": "code", "execution_count": 20, "id": "8972b01d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Got: 1\n", "Accumulated: 3\n" ] } ], "source": [ "print(f\"Accumulated: {generator.send(1)}\")" ] }, { "cell_type": "markdown", "id": "2e9d9826", "metadata": {}, "source": [ "Мы передали единичку, о чем и говорим. Если мы ничего не передадим, а просто пойдем дальше, то у нас по условию выхода из цикла, как вы помните, заканчивается наш генератор и выбрасывается исключение `StopIteration`." ] }, { "cell_type": "code", "execution_count": 21, "id": "13dace76", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Got: None\n" ] }, { "ename": "StopIteration", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mgenerator\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mStopIteration\u001b[0m: " ] } ], "source": [ "next(generator)" ] }, { "cell_type": "markdown", "id": "6e6b2588", "metadata": {}, "source": [ "Видите, мы ничего не получили, мы получили `None`, поэтому мы закончили выполнение.\n", " \n", "Итак, генераторы являются очень важной концепцией, которая используется во многих особенностях языка Python, например, в асинхронном программировании. Они позволяют вам создавать функции, к которым можно возвращаться, которые хранят контекст выполнения и позволяют вам оптимально работать с памятью." ] } ], "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.8.8" } }, "nbformat": 4, "nbformat_minor": 5 }