You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

563 lines
21 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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<ipython-input-5-c186e1cb4ee7>\u001b[0m in \u001b[0;36m<module>\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<ipython-input-12-323ce5d717bb>\u001b[0m in \u001b[0;36m<module>\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<ipython-input-21-323ce5d717bb>\u001b[0m in \u001b[0;36m<module>\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
}