Yield python как работает

woman 1754895 1920 Советы на день

Ключевое слово yield в Python

Python предоставляет программисту большой набор инструментов, один из которых — yield. Он заменяет обычный возврат значений из функции и позволяет сэкономить память при обработке большого объема данных.

yield – один из тех инструментов, использовать которые вовсе не обязательно. Всё, что можно реализовать с его помощью, можно сделать, используя обычный возврат return. Однако этот оператор позволяет не только сэкономить память, но и реализовать взаимодействие между несколькими последовательностями в пределах одного цикла.

Что такое yield и как это работает

Yield – ключевое слово, которое используется вместо return. С его помощью функция возвращает значение без уничтожения локальных переменных, кроме того, при каждом последующем вызове функция начинает своё выполнение с оператора yield.

Функция, содержащая yield в Python 3, называется генератором. Чтобы разобраться, как работает yield и зачем его используют, необходимо узнать, что такое генераторы, итераторы и итерации.

Но перед этим рассмотрим пример:

Тип полученного значения при вызове функции — это генератор. Один из способов получения значений из генератора — это их перебрать в цикле for. Им мы и воспользовались. Но можно его легко привести к списку, как мы сделали в статье про числа Фибоначчи.

Теперь разберемся, как это всё работает.

Что такое итерации

В программировании итерация — это процесс, в котором последовательно повторяется набор инструкций определенное количество раз или до тех пор, пока не будет выполнено условие.

Цикл — это повторяющаяся последовательность команд, каждый цикл состоит из итераций. То есть, одно выполнение цикла — это итерация. Например, если тело цикла выполнилось 5 раз, это значит, что прошло 5 итераций.

Итератор — это объект, позволяющий «обходить» элементы последовательностей. Программист может создать свой итератор, однако в этом нет необходимости, интерпретатор Python делает это сам.

Что такое генераторы

Генератор — это обычная функция, которая при каждом своём вызове возвращает объект. При этом в функции-генераторе вызывается next.

Отличие генераторов от обычной функции состоит в том, что функция возвращает только одно значение с помощью ключевого слова return, а генератор возвращает новый объект при каждом вызове с помощью yield. По сути генератор ведет себя как итератор, что позволяет использовать его в цикле for.

Программист может не использовать генераторы, однако в некоторых ситуациях оптимизировать программу можно только с их помощью.

Помимо yield, есть и другие способы создания генераторов, они описаны в статье о генераторах списка.

Функция next()

Эта функция позволяет извлекать следующий объект из итератора. То есть чтобы цикл перешел с текущей итерации на следующую, вызывается функция next(). Когда в итераторе заканчиваются элементы, возвращается значение, заданное по умолчанию, или возбуждается исключение StopItered.

На самом деле каждый объект имеет встроенный метод __next__, который и обеспечивает обход элементов в цикле, а функция next() просто вызывает его.

Вот пример использования next:

Преимущества использования yield

yield используют не потому, что это определено синтаксисом Python, ведь всё, что можно реализовать с его помощью, можно реализовать и с помощью обычного return.

Функция, которая обрабатывает большую последовательность и использует обычный return, требует от интерпретатора выделять ей много памяти. И если обычно такие функции не сильно влияют на производительность программы, то в проектах, содержащих последовательности с миллионами элементов, они потребляют очень много памяти.

Использование yield в языке программирования Python 3 позволяет не сохранять в память всю последовательность, а просто генерирует объект при каждом вызове функции. Это позволяет обойтись без использования большого количества оперативной памяти.

Сравнение производительности return и yield

Часто yield используют, когда необходимо прочитать большой текстовый файл. Чтобы наглядно показать преимущество использования генераторов, нужно создать два скрипта:

Затем скрипты должны обработать несколько файлов разных размеров, при этом получаются следующие результаты:

Размер файла return yield
Память Время Память Время
4 Кбайт 5,3 Мбайт 0.023 с 5,42 Мбайт 0.08 c
324 Кбайт 9,98 Мбайт 0.028 с 5,37 Мбайт 0,32 с
26 Мбайт 392 Мбайт 27 с 5.52 Мбайт 29.61 с
263 Мбайт 3,65 Гбайт 273.56 с 5,55 Мбайт 292,99 с

Видно, что в обоих случаях время увеличивается с примерно одинаковой скоростью, а количество потребляемой памяти сильно различается. Чем больше обрабатываемый файл, тем заметнее различие.

yield from

Многие считают, что yield from был добавлен в язык Python 3, чтобы объединить две конструкции: yield и цикл for, потому что они часто используются совместно, как в следующем примере:

Однако истинное предназначение нововведения немного в другом. Конструкция позволяет «вкладывать» один генератор в другой, то есть создавать субгенераторы.

yield from позволяет программисту легко управлять сразу несколькими генераторами, настраивать их взаимодействие и, конечно, заменить более длинную конструкцию for+yield, например:

Как видно из примера, yield from позволяет одному генератору получать значения из другого. Этот инструмент сильно упрощает жизнь программиста, особенно при асинхронном программировании.

Заключение

Использование генераторов в правильных местах позволяет значительно уменьшить потребление памяти, кроме того, взаимодействие с генераторами более прозрачно и легче поддается отладке.

yield – это лишь одно из многих полезных средств языка Python, которое может быть без проблем заменено обычным возвратом из функции с помощью return. Оно добавлено в язык, чтобы оптимизировать производительность программы, упростить код и его отладку и дать программистам возможность применять необычные решения в специализированных проектах.

Источник

Как работает yield

На StackOverflow часто задают вопросы, подробно освещённые в документации. Ценность их в том, что на некоторые из них кто-нибудь даёт ответ, обладающий гораздо большей степенью ясности и наглядности, чем может себе позволить документация. Этот — один из них.

Вот исходный вопрос:

Как используется ключевое слово yield в Python? Что оно делает?

Например, я пытаюсь понять этот код (**):

Что происходит при вызове метода _get_child_candidates? Возвращается список, какой-то элемент? Вызывается ли он снова? Когда последующие вызовы прекращаются?

** Код принадлежит Jochen Schulz (jrschulz), который написал отличную Python-библиотеку для метрических пространств. Вот ссылка на исходники: http://well-adjusted.de/

Итераторы

Для понимания, что делает yield, необходимо понимать, что такое генераторы. Генераторам же предшествуют итераторы. Когда вы создаёте список, вы можете считывать его элементы один за другим — это называется итерацией:

Mylist является итерируемым объектом. Когда вы создаёте список, используя генераторное выражение, вы создаёте также итератор:

Всё, к чему можно применить конструкцию «for… in. », является итерируемым объектом: списки, строки, файлы… Это удобно, потому что можно считывать из них значения сколько потребуется — однако все значения хранятся в памяти, а это не всегда желательно, если у вас много значений.

Генераторы

Генераторы это тоже итерируемые объекты, но прочитать их можно лишь один раз. Это связано с тем, что они не хранят значения в памяти, а генерируют их на лету:

Всё то же самое, разве что используются круглые скобки вместо квадратных. НО: нельзя применить конструкцию for i in mygenerator второй раз, так как генератор может быть использован только единожды: он вычисляет 0, потом забывает про него и вычисляет 1, завершаяя вычислением 4 — одно за другим.

Yield

Yield это ключевое слово, которое используется примерно как return — отличие в том, что функция вернёт генератор.

В данном случае пример бесполезный, но это удобно, если вы знаете, что функция вернёт большой набор значений, который надо будет прочитать только один раз.

Чтобы освоить yield, вы должны понимать, что когда вы вызываете функцию, код внутри тела функции не исполняется. Функция только возвращает объект-генератор — немного мудрёно 🙂

Ваш код будет вызываться каждый раз, когда for обращается к генератору.

Теперь трудная часть:

В первый запуск вашей функции, она будет исполняться от начала до того момента, когда она наткнётся на yield — тогда она вернёт первое значение из цикла. На каждый следующий вызов будет происходить ещё одна итерация написанного вами цикла, возвращаться будет следующее значение — и так пока значения не кончатся.

Генератор считается пустым, как только при исполнении кода функции не встречается yield. Это может случиться из-за конца цикла, или же если не выполняется какое-то из условий «if/else».

Объяснение кода из исходного вопроса

Читатель может остановиться здесь, или же прочитать ещё немного о продвинутом использовании генераторов:

Контроль за исчерпанием генератора

Это может оказаться полезным для разных целей вроде управления доступом к какому-нибудь ресурсу.

Ваш лучший друг Itertools

Модуль itertools содержит специальные функции для работы с итерируемыми объектами. Желаете продублировать генератор? Соединить два генератора последовательно? Сгруппировать значения вложенных списков в одну строчку? Применить map или zip без создания ещё одного списка?

Просто добавьте import itertools.

Хотите пример? Давайте посмотрим на возможные порядки финиширования на скачках (4 лошади):

Понимание внутреннего механизма итерации

Итерация это процесс, включающий итерируемые объекты (реализующие метод __iter__()) и итераторы (реализующие __next__()). Итерируемые объекты это любые объекты, из которых можно получить итератор. Итераторы это объекты, позволяющие итерировать по итерируемым объектам.

Больше информации по данному вопросу доступно в статье про то, как работает цикл for.

Источник

Как использовать 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() на объекте, переданном в качестве параметра.

Это означает, что вы можете создавать свои собственные итерационные таблицы:

Источник

Итерируемый объект, итератор и генератор

Привет, уважаемые читатели Хабрахабра. В этой статье попробуем разобраться что такое итерируемый объект, итератор и генератор. Рассмотрим как они реализованы и используются. Примеры написан на Python, но итераторы и генераторы, на мой взгляд, фундаментальные понятия, которые были актуальны 20 лет назад и еще более актуальны сейчас, при этом за это время фактически не изменились.

image loader

Итераторы

Для начала вспомним, что из себя представляет паттерн «Итератор(Iterator)».
Назначение:

Существуют два вида итераторов, внешний и внутренний.
Внешний итератор — это классический (pull-based) итератор, когда процессом обхода явно управляет клиент путем вызова метода Next.
Внутренний итератор — это push-based-итератор, которому передается callback функция, и он сам уведомляет клиента о получении следующего элемента.

Классическая диаграмма паттерна “Итератор”, как она описана в небезызвестной книги «банды четырех»:
image loader

Aggregate — составной объект, по которому может перемещаться итератор;
Iterator — определяет интерфейс итератора;
ConcreteAggregate — конкретная реализация агрегата;
ConcreteIterator — конкретная реализация итератора для определенного агрегата;
Client — использует объект Aggregate и итератор для его обхода.

Пробуем реализовать на Python классический итератор

Конкретная реализация итератора для списка:

Конкретная реализация агрегата:

Теперь мы можем создать объект коллекции и обойти все ее элементы с помощью итератора:

А так как мы реализовали метод first, который сбрасывает итератор в начальное состояние, то можно воспользоваться этим же итератором еще раз:

Реализации могут быть разные, но основная идея в том, что итератор может обходить различные структуры, вектора, деревья, хеш-таблицы и много другое, при этом имея снаружи одинаковый интерфейс.

Протокол итерирования в Python

В книге «банды четырех» о реализации итератора написано:

Минимальный интерфейс класса Iterator состоит из операций First, Next, IsDone и CurrentItem. Но если очень хочется, то этот интерфейс можно упростить, объединив операции Next, IsDone и CurrentItem в одну, которая будет переходить к следующему объекту и возвращать его. Если обход завершен, то эта операция вернет специальное значения(например, 0), обозначающее конец итерации.

Именно так и реализовано в Python, но вместо специального значения, о конце итерации говорит StopIteration. Проще просить прощения, чем разрешения.

Сначала важно определиться с терминами.

Рассмотрим итерируемый объект (Iterable). В стандартной библиотеке он объявлен как абстрактный класс collections.abc.Iterable:

У него есть абстрактный метод __iter__ который должен вернуть объект итератора. И метод __subclasshook__ который проверяет наличие у класса метод __iter__. Таким образом, получается, что итерируемый объект это любой объект который реализует метод __iter__

Но есть один момент, это функция iter(). Именно эту функцией использует например цикл for для получения итератора. Функция iter() в первую очередь для получения итератора из объекта, вызывает его метод __iter__. Если метод не реализован, то она проверяет наличие метода __getitem__ и если он реализован, то на его основе создается итератор. __getitem__ должен принимать индекс с нуля. Если не реализован ни один из этих методов, тогда будет вызвано исключение TypeError.

Итого, итерируемый объект — это любой объект, от которого встроенная функция iter() может получить итератор. Последовательности(abc.Sequence) всегда итерируемые, поскольку они реализуют метод __getitem__

Теперь посмотрим, что с итераторами в Python. Они представлены абстрактным классом collections.abc.Iterator:

__next__ Возвращает следующий доступный элемент и вызывает исключение StopIteration, когда элементов не осталось.
__iter__ Возвращает self. Это позволяет использовать итератор там, где ожидается итерируемых объект, например for.
__subclasshook__ Проверяет наличие у класса метода __iter__ и __next__

Итого, итератор в python — это любой объект, реализующий метод __next__ без аргументов, который должен вернуть следующий элемент или ошибку StopIteration. Также он реализует метод __iter__ и поэтому сам является итерируемым объектом.

Таким образом можно реализовать итерируемый объект на основе списка и его итератор:

Функция next() вызывает метод __next__. Ей можно передать второй аргумент который она будет возвращать по окончанию итерации вместо ошибки StopIteration.

Прежде чем переходить к генераторам, рассмотрим еще одну возможность встроенной функции iter(). Ее можно вызывать с двумя аргументами, что позволит создать из вызываемого объекта(функция или класс с реализованным методом __call__) итератор. Первый аргумент должен быть вызываемым объектом, а второй — неким ограничителем. Вызываемый объект вызывается на каждой итерации и итерирование завершается, когда возбуждается исключение StopIteration или возвращается значения ограничителя.

Например, из функции которая произвольно возвращает 1-6, можно сделать итератор, который будет возвращать значения пока не «выпадет» 6:

Небольшой класс ProgrammingLanguages, у которого есть кортеж c языками программирования, конструктор принимает начальное значения индекса по названию языка и функция __call__ которая перебирает кортеж.

Можем перебрать все языки начиная с C# и до последнего:

Генераторы

С точки зрения реализации, генератор в Python — это языковая конструкция, которую можно реализовать двумя способами: как функция с ключевым словом yield или как генераторное выражение. В результате вызова функции или вычисления выражения, получаем объект-генератор типа types.GeneratorType.

В объекте-генераторе определены методы __next__ и __iter__, то есть реализован протокол итератора, с этой точки зрения, в Python любой генератор является итератором.
Концептуально, итератор — это механизм поэлементного обхода данных, а генератор позволяет отложено создавать результат при итерации. Генератор может создавать результат на основе какого то алгоритма или брать элементы из источника данных(коллекция, файлы, сетевое подключения и пр) и изменять их.

Ярким пример являются функции range и enumerate:

range генерирует ограниченную арифметическую прогрессию целых чисел, не используя никакой источник данных.
enumerate генерирует двухэлементные кортежи с индексом и одним элементом из итерируемого объекта.

Yield

Для начало напишем простой генератор не используя объект-генератор. Это генератор чисел Фибоначчи:

Но используя ключевое слово yield можно сильно упростить реализацию:

Любая функция в Python, в теле которой встречается ключевое слово yield, называется генераторной функцией — при вызове она возвращает объект-генератор.
Объект-генератор реализует интерфейс итератора, соответственно с этим объектом можно работать, как с любым другим итерируемым объектом.

Рассмотрим работу yield:

Создается стейт-машина в которой при каждом вызове __next__ меняется состояния и в зависимости от него вызывается тот или иной кусок кода. Если в функции yield в цикле, то соответственно состояние стейт-машины зацикливается пока не будет выполнено условие.

Свой вариант range:

Генераторное выражение (generator expression)

Если кратко, то синтаксически более короткий способ создать генератор, не определяя и не вызывая функцию. А так как это выражение, то у него есть и ряд ограничений. В основном удобно использовать для генерации коллекций, их несложных преобразований и применений на них условий.

В языках программирования есть такие понятия, как ленивые/отложенные вычисления(lazy evaluation) и жадные вычисления(eager/greedy evaluation). Генераторы можно считать отложенным вычислением, в этом смысле списковое включение(list comprehension) очень похожи на генераторное выражение, но являются разными подходами.

Первый вариант работает схожим с нашей функцией cool_range образом и может генерировать без проблем любой диапазон. А вот второй вариант создаст сразу целый список, со всеми вытекающими от сюда проблемами.

Yield from

Для обхода ограниченно вложенных структур, традиционный подход использовать вложенные циклы. Тот же подход можно использовать когда генераторная функция должна отдавать значения, порождаемые другим генератором.

Функция похожая на itertools.chain:

Но вложенные циклы можно убрать, добавив конструкцию yield from:

Основная польза yield from в создании прямого канала между внутренним генератором и клиентом внешнего генератора. Но это уже больше тема про сопрограммы(coroutines), которые заслуживают отдельной статьи. Там же можно обсудить методы генератора: close(), throw() и send().

И в заключении еще один пример. Функция принимающая итерируемый объект, с любым уровнем вложенности другими итерируемыми объектами, и формирующая плоскую последовательность:

Источник

Оцените статью
Добавить комментарий

Adblock
detector