что такое yield в python
Python: Объяснение работы yield, итераторов и генераторов

Для понимания что делает «yield», вы должны понимать что такое генераторы. Для понимания что такое генераторы — должны знать об итераторах и итерируемых объектах.
Итерируемые объекты (iterables)
Хочется назвать их неправильным, с точки зрения русского языка, словом «итерабельные» — т.е. те, по которым может происходить итерация. Но, правильнее будет назвать их «итерируемые», хотя лично меня это слово слегка путает.
Когда вы создаёте список (list) вы можете считывать его элементы по одному — это называется итерацией.
Lst — итерируемый объект (iterable). Когда вы используете списковые выражения (list comprehensions), вы создаёте список — итерируемый объект:
Любое объект который вы можете использовать в конструкции «for … in …» является итерирумым: списки, строки, файловые объекты и т.п.. Итерирумые объекты достаточно удобны потому что вы можете считывать из них столько данных, сколько вам необходимо, но при этом вы храните все значения последовательности в памяти и это не всегда приемлемо, особенно если вы имеете достаточно большие последовательности.
Генераторы
Генераторы — итерируемые объекты, но, в общем случае, вы можете их использовать только один раз. Это связано с тем, что они не хранят все значения в памяти, а генерируют значения «на лету» — по мере запроса:
Код выглядит почти так же, как и в предыдущем примере, только вместо квадратных скобочек («[ ]») были использованы круглые («( )»). Заметьте, что вы не можете выполнить цикл по generator во второй раз, поскольку ничего в памяти не хранится, попытка пройтись второй раз будет просто проигнорирована, т.к. generator выбросит при первом запросе на получение следующего значения StopIterationError, однако, вы это не заметите, если будете использовать цикл for, это исключение будет перехвачено и интерпретировано как конец цикла). Но вручную это можно проверить:
next — это метод для получения следующего значения генератора, если вы его используете не в цикле for.
Yield
Yield — это ключевое слово которое используется так же, как и слово return. Разница в том, что функция при этом начинает возвращать генератор вместо значения.
В данном случае, с практической точки зрения, это бесполезный пример. Ощутимую пользу вы получите в ситуации, когда ваша функция должна будет возвращать достаточно большой объём данных, но использовать их надо будет только один раз.
При первом исполнении кода тела функции код будет выполнен с начала и до первого встретившегося оператора yield. После этого будет возвращено первое значение и выполнение тела функции опять приостановлено. Запрос следующего значения у генератора во время итерации заставит код тела функции выполняться дальше (с предыдущего yield’а), пока не встретится следующий yield. Генератор считается «закончившимся» в случае если при очередном исполнении кода тела функции не было встречено ни одного оператора yield.
Как использовать yield и генераторы в Python
Генераторы – это потрясающая особенность Python и важный шаг в освоении языка. Поняв их, вы уже не сможете без них обойтись.
Напомним об итерациях
Когда вы читаете элементы по одному из списка, это называется итерацией:
В цикле for мы берем его элементы один за другим, и таким образом выполняем итерацию:
Каждый раз, когда вы можете использовать “for… in…” для чего-либо, это итерабельный объект: списки, строки, файлы…
Эти итерационные таблицы удобны тем, что вы можете читать их сколько угодно. Но это не всегда оптимально, поскольку вам приходится хранить все элементы в памяти.
Генераторы
Таким же образом можем создавать генерируемые значения:
Единственное отличие от предыдущего варианта заключается в том, что вместо [] используется ().
Но вы не можете прочитать генератор второй раз, потому что принцип работы генераторов заключается в том, что они генерируют все на лету: здесь он вычисляет 0, потом забывает его, потом вычисляет 1, потом забывает его, потом вычисляет 4. Все это по очереди.
Ключевое слово yield
yield – это слово, используемое вместо return. Только в этом случае мы получим генератор.
В данном случае это бесполезный пример, но в реальной жизни он полезен, когда вы знаете, что функция вернет много значений, которые вы хотите прочитать только один раз.
Секрет мастеров дзен, которые обрели понимание yield, заключается в том, что когда вы вызываете функцию, код функции не выполняется. Вместо этого функция вернет объект генератора.
Это нелегко понять, поэтому прочитайте эту часть несколько раз.
CreateGenerator() не выполняет код CreateGenerator.
CreateGenerator() возвращает объект генератора.
На самом деле, пока вы не трогаете генератор, ничего не происходит. Затем, как только мы начинаем итерировать генератор, запускается код функции.
При первом выполнении кода он начнет с начала функции, дойдет до yield и вернет первое значение.
Затем, при каждом новом цикле, код будет выполняться с того места, где он остановился (да, Python сохраняет состояние кода генератора между каждым вызовом), и выполнять код снова, пока не встретится с yield. Поэтому в нашем случае он будет зациклен.
Так будет продолжаться до тех пор, пока код не перестанет удовлетворять условию yield, и, следовательно, не будет больше возвращаться значение.
В этом случае генератор считается окончательно пустым.
Его нельзя перемотать, нужно создать другой.
Причина, по которой код больше не удовлетворяет условию yield, выбирается вами: условие if/else, цикл, рекурсия…
Конкретный пример
yield не только экономит память, но и скрывает сложность алгоритма за классическим API итерации.
Предположим, у вас есть функция, которая извлекает слова длиной более 3 символов из всех файлов в папке.
Он может выглядеть следующим образом:
У вас есть алгоритм, который полностью скрывает свою сложность, потому что с точки зрения пользователя он просто делает это:
И для него это открыто.
Более того, он может использовать все инструменты, которые мы обычно используем для итераций.
Все функции, которые принимают итерабельные значения, принимают результат функции в качестве параметра благодаря магии duck typing.
Таким образом, создается прекрасный набор инструментов.
Контроллер yield
Пока есть в наличии, вы можете получить столько автомобилей, сколько захотите.
Как только запасов не останется…
И это справедливо для любого нового генератора:
itertools: ваш новый любимый модуль
С генераторами нужно работать с учетом их природы: вы можете прочитать их только один раз и не можете заранее определить их длину.
itertools – это модуль, специализирующийся на этом: map, zip, slice…
Он содержит функции, которые работают со всеми итерациями, включая генераторы.
Построим две итерабельные строки и возьмем первые 10 символов?
За кулисами итерации
Под капотом все iterables используют генератор под названием ” итератор”. Вы можете получить его, используя функцию iter() для итератора:
Итераторы имеют метод next(), который возвращает значение для каждого вызова метода.
Когда больше нет значений, они выбрасывают исключение StopIteration:
Для всех тех, кто думает, что я выдумываю, когда говорю, что в Python мы используем исключения для управления потоком программы: это механизм внутреннего цикла в Python.
Циклы for используют функцию iter() для создания генератора, а затем перехватывают исключение для остановки. В каждом цикле for вы, сами того не зная, вызываете исключение.
Для справки, текущая реализация заключается в том, что iter() вызывает метод iter() на объекте, переданном в качестве параметра.
Это означает, что вы можете создавать свои собственные итерационные таблицы:
Ключевое слова yield и генераторы в Python
Отличия генераторов и списков.
Давайте на практике разберемся в отличии списков от генераторов. Для начала создадим список: Как и ожидается тип переменной simple_list является список и перебор элементов выводит ожидаемый результат.
Важно отметить, что функция-генератор square_num не хранит ни одного элемента в памяти, а значения вычисляются во время выполнения, возвращаются и удаляются из памяти. Единственной что храниться постоянно в память в таком случаи это данные состояния генератора, которые обычно намного меньше, чем большие списки.
Для получения данных из функции-генератора вместо постоянного использования метода next можно использовать цикл for для итерации по её значениям.
Оптимизация производительности.
Как уже говорилось ранее, генераторы удобны, когда речь заходит о задачах в которых обрабатывается большие объемы данных. Поскольку элементы в генераторе создаются в момент вызова и удаляются сразу после использования, объем требуемой памяти значительно сокращается.
В предыдущих примерах выигрыша от использования генератора не было, но давайте рассмотрим ситуацию в которой мы будем работать со 1000000 элементов и посмотрим как расходуется память при использовании генератора по сравнению с обычным списком.
В результате выполнения данного кода я получил следующий результат(ваш может выглядеть иначе):
До вызова функции использовалось 13 MB памяти, а после создания списка из 1000000 элементов занимаемая память выросла до 196 MB. При этом время необходимое на выполнение операции составило 13,6 секунды.
Теперь давайте используем функцию-генератор. Разница в коде совсем небольшая, как вы можете заметить ниже:
А вот какие результаты я получил на своем компьютере при выполнении этого кода:
Как вы можете убедиться при использовании генератора, для той же задачи, количество используемой память, в ходе выполнения скрипта, не изменилось, а скорость его выполнения значительно сократилось.
Надеюсь после прочтения статьи вы стали лучше понимать для чего используется ключевое слово yield в Python, а так же преимущества использования генераторов в вашей работе.
Генераторы Python. Их создание и использование
Приходилось ли вам когда-либо работать с настолько большим набором данных, что он переполнял память вашего компьютера? Или быть может у вас была сложная функция, для которой нужно было бы сохранять внутреннее состояние при вызове? А если при этом функция была слишком маленькой, чтобы оправдать создание собственного класса? Во всех этих случаях вам придут на помощь генераторы Python и ключевое слово yield.
Прочитав эту статью, вы узнаете:
Если вы являетесь Питонистом начального или среднего уровня и вы заинтересованы в том, чтобы научиться работать с большими наборами данных в питоновском стиле, то скорее всего это руководство для вас.
По ссылке ниже вы можете скачать копию файла с данными, используемыми в этом руководстве.
Использование Генераторов
Функции генераторов (их описание можно почитать в PEP 255) представляют собой особый вид функций, которые возвращают «ленивый итератор». И хотя содержимое этих объектов вы можете перебирать также как и списки, но при этом, в отличие от списков, ленивые итераторы не хранят свое содержимое в памяти. Чтобы составить общее представление об итераторах в Python взгляните на статью Python “for” Loops (Definite Iteration).
Теперь, когда вы имеете примерное представление о том, чем является генератор, у вас наверняка появилось желание увидеть как он работает. Давайте рассмотри два примера. В первом вы увидите общий принцип работы генераторов. В последующих у вас будет возможность изучить работу генераторов более подробно.
Пример 1: Чтение больших файлов
Списки Python
Работа с потоками данных и большими файлами, такими например как CSV, являются наиболее распространенными вариантами использования генераторов. Давайте возьмем CSV файл (CSV является стандартным форматом для обмена данными, колонки в нем разделяются при помощи запятых). Предположим, что вы хотите посчитать количество имеющихся в нем рядов. Код ниже предлагает один из путей для, того, чтобы осуществить это:
Это вполне приемлемое решение, но будет ли этот подход работать, если файл окажется слишком большим? А что если файл окажется больше чем вся доступная память, которая есть в нашем распоряжении? Для того чтобы ответить на этот вопрос, давайте предположим, что csv_reder() будет открывать файл и считывать его в массив.
В этом случае open() возвращает объект генератора, который вы можете «лениво» (не обсчитывая заранее) перебирать ряд за рядом. Тем не менее, file.read().split() загружает все данные в память сразу, вызывая ошибку памяти (MemoryError).
До того как это произойдет, вы можете заметить, что ваш компьютер замедлился. Возможно вам потребуется даже вручную остановить программу. Но что нам делать, если мы хотим этого избежать?
Генераторы Python
Давайте взглянем на новое определение функции csv_reader() :
В этой версии вы открываете файл и проходите его содержимое, возвращая ряд за рядом. Этот код выводит следующий результат без каких-либо ошибок:
Почему так получилось? Да потому что вы по сути превратили функцию csv_reader() в генератор. Эта версия кода открывает файл, проходит по строкам и извлекает для чтения лишь отдельный ряд, вместо того, чтобы возвращать весь файл целиком.
Также вы можете определить выражение создающее генератор, которое очень похоже по синтаксису на выражение создающее список. В таком виде вы можете использовать генератор без вызова функции:
Такой способ создания генератора csv_gen является более лаконичным.
Более подробно о yield мы расскажем позже, а пока запомните основные отличия между использованием ключевых слов yield и return:
Пример 2: Создание бесконечной последовательности
Создание же бесконечной последовательности стопроцентно потребует от нас использования генератора. Причина проста — ограниченность памяти нашего компьютера.
Если вы попробуете запустить этот код в теле цикла for, то увидите, что на самом деле он бесконечный:
Эта программа будет исполняться, до тех пор, пока вы ее вручную не остановите.
Пример 3: Нахождение палиндромов
Вы можете использовать бесконечные последовательности множеством различных способов. Одним из них, который мы отметим особенно, является создание детектора палиндромов. Детектор палиндромов выявляет все последовательности букв и цифр, которые являются палиндромами. Это слова или числа, которые читаются одинаково вперед и назад, как «121» например. Сперва давайте зададим наш числовой детектор палиндромов:
Не особо беспокойтесь о понимании вычислений, лежащих в основе данного кода. Просто заметьте, что функция принимает введенное число, переворачивает его, и сравнивает с оригиналом. Теперь вы можете использовать генератор бесконечной последовательности для получения бегущего списка со всеми числовыми палиндромами:
В консоли выводятся только те номера, которые читаются одинаково и вперед и назад.
Примечание: на практике вам вряд ли придется писать свой собственный бесконечный генератор последовательностей, по той простой причине, что есть уже очень эффективный генератор itertools.count() из модуля itertools.
Теперь, когда вы познакомились с простым примером использования генератора бесконечной последовательности, давайте рассмотрим более детально работу этого генератора.
Понимание работы генератора Python
К этому моменту вы уже познакомились с двумя основными способами создания генераторов: с помощью функции и с помощью выражения. У вас также должно было сформироваться интуитивное представление о том, как работает генератор. Давайте теперь уделим некоторое время тому, чтобы сделать наши знания более четкими.
Примечание. Если вы хотите больше узнать о генераторах списков, множеств и словарей в Python, можете прочитать статью Эффективное использование генераторов списков (англ).
Создание генератора с помощью выражения
Как и выражения создающие списки, выражения создающие генераторы позволяют быстро получить объект генератора с помощью всего одной строчки кода. Использоваться они могут в тех же случаях, что и выражения создающие списки, но при этом у них есть одно дополнительное преимущество. Их можно создавать не удерживая весь объект в памяти перед итерацией. Если перефразировать, вы не будете расходовать память при использовании генератора.
Давайте для примера возьмем возведение в квадрат некоторых чисел:
Это подтверждает тот факт, что с помощью круглых скобок вы создали объект генератора, а также то, что он отличается от списка.
Профилирование эффективности генератора
Ранее мы узнали, что использование генераторов является отличным способом оптимизации памяти. И хотя генератор бесконечной последовательности является наиболее ярким примером этой оптимизации, давайте рассмотрим еще один пример с возведением числа в квадрат и проверим размер полученных объектов.
Вы можете сделать это с помощью вызова функции sys.getsizeof () :
В этом случае размер списка, полученного с помощью выражения составляет 87 624 байта, а размер генератора — только 120. То есть, список занимает памяти в 700 раз больше, чем генератор! Однако нужно помнить одну вещь. Если размер списка меньше доступной памяти на работающей машине, тогда обработка его будет занимать меньше времени, чем аналогичная обработка генератора. Чтобы удостовериться в этом, давайте просуммируем результаты приведенных выше выражений. Вы можете использовать для анализа функцию cProfile.run () :
Здесь вы можете видеть, что суммирование всех значений, содержащихся в списке заняло около трети времени аналогичного суммирования с помощью генератора. Поэтому если скорость является для вас проблемой, а память — нет, то список, возможно, окажется лучшим инструментом для работы.
Примечание. Эти измерения действительны не только для генераторов, созданных с помощью выражений. Они абсолютно идентичны и для генераторов, созданных с помощью функции. Ведь, как мы уже говорили выше, эти генераторы эквивалентны.
Генераторы в Python и их отличие от списков и функций
В этом уроке мы с вами разберем, что из себя представляют генераторы в программировании на языке Python. Кроме того, мы обсудим генераторные выражения и разницу между списками и генераторами, а также между функциями и генераторами.
1. Что такое генераторы в Python?
Генератор это подвид итерируемых объектов, как список или кортеж. Он генерирует для нас последовательность значений, которую мы можем перебрать. Эту последовательность можно использовать для итерации в цикле for, но нельзя проиндексировать (т. е., перебрать ее можно только один раз). Давайте посмотрим, как создается такая последовательность значений при помощи генератора.
а. Синтаксис генератора в Python 3
Для создания генератора в Python внутри функции вместо ключевого слова return используется ключевое слово yield. Обратите внимание на пример:
В этом примере мы определили генератор с именем counter() и назначили значение 1 локальной переменной i. Цикл while будет выполняться, пока i меньше или равно 10. Внутри цикла мы возвращаем (yield) значение i и увеличиваем его на единицу.
Затем мы используем этот генератор в цикле for.
b. Как работает генератор в Python
Чтобы разобраться в том, как работает этот код, давайте начнем с цикла for. Этот цикл выводит каждый элемент генератора (т. е., каждый элемент, возвращаемый генератором).
Мы начинаем с i=1. Таким образом, первый элемент, возвращаемый генератором, это 1. Цикл for выводит этот элемент на экран благодаря ключевому слову print. Затем i инкрементируется до 2. Весь процесс повторяется, пока i не инкрементируется до 11 (т. е., пока условие в цикле while не даст false).
Но если вы забудете добавить инкремент i, вы получите бесконечный генератор. Дело в том, что генератору в каждый момент времени нужно удерживать в памяти только одно значение. Таким образом, нет никаких ограничений памяти.
EvenTraceback (самый недавний вызов идет последним):
Поскольку 2 это четное число, 2%2 это всегда 0. Поэтому условие в цикле while всегда будет соблюдаться (всегда true). В результате генератор even() продолжает возвращать значение Even, пока мы не прервем выполнение цикла вручную (сочетанием клавиш Ctrl+C).
Обратите внимание, что генератор может содержать больше одного ключевого слова yield. Примерно так же, как функция может иметь больше одного ключевого слова return.
2. Возврат значений в список
Здесь все просто. Если вы примените функцию list() к вызову генератора, она вернет список возвращенных генератором значений, в том порядке, в котором они возвращались. В следующем примере генератор возвращает квадраты чисел, если эти квадраты четные.
Чтобы создать список из возвращаемых генератором значений, мы просто применяем функцию list() к вызову генератора. Мы не перебираем эти значения при помощи цикла for.
Как видите, для чисел в диапазоне 0-9 (не 10, потому что диапазон (10) это числа 0-9), четные квадраты это 0, 4, 16, 36 и 64. Остальные — 1, 9, 25, 49, 81 — нечетные. Поэтому они не возвращаются генератором.
3. Разница между списком и генератором в Python
Разница между ними очень проста. Список сразу удерживает в памяти определенное число значений. А генератор в каждый отдельный момент удерживает только одно значение — то, которое он возвращает. Вот почему генераторы требуют куда меньше памяти. Когда мы применяем генератор, нам также не приходится ждать рендеринга всех значений.
4. Разница между генератором и функцией в Python
Чтобы разобраться в различиях между генераторами и функциями, давайте сначала разберем разницу между ключевыми словами return и yield.
Когда интерпретатор доходит до ключевого слова return, выполнение функции полностью прекращается. Но когда он доходит до ключевого слова yield, программа приостанавливает выполнение функции и возвращает значение в итерируемый объект. После этого интерпретатор возвращается к генератору, чтобы повторить процесс для нового значения.
Кроме того, при прекращении выполнения функции ее локальные переменные стираются. В генераторах ситуация другая. Взгляните:
5. Генераторные выражения в Python
Для создания генераторов на скорую руку можно использовать выражения (как и для генераторов списка). Давайте возьмем для этого список:
Как видим, мы получили генератор. Но чтобы получить доступ к значениям, нужно сохранить его в переменной, а затем применить к этой переменной функцию next().
Traceback (самый недавний вызов идет последним):
Вот и все, что мы хотели рассказать вам о генераторах в Python. Надеемся, вам понравилось наше объяснение.
6. Заключение
Теперь, когда вы знаете о преимуществах генераторов по сравнению со списками и функциями, вы понимание их важность. Что-то мы можем делать при помощи генератора, что-то — при помощи функции или даже генератора списка. Но использование генераторов наиболее эффективно.

