Что происходит при итерации по генератору

Что происходит при итерации по генератору

Новые структурные элементы в Python 2.2

Серия контента:

Этот контент является частью # из серии # статей: Очаровательный Python

Этот контент является частью серии: Очаровательный Python

Следите за выходом новых статей этой серии.

Добро пожаловать в мир экзотического управления потоками данных. В Python 2.2 разработчики получают возможности, которые раньше были недоступны, или, по крайней мере, не так практичны, в более ранних версиях языка.

И хотя то, что предлагает Python 2.2, не настолько умопомрачительно, как, например, полные продолжения (continuations) и микронити (microthreads), представленные в Stackless Python, генераторы и итераторы действительно могут кое-что, что выделяет их среди традиционных функций и классов.

Давайте рассмотрим сначала итераторы, поскольку их легче понять. Прежде всего, итератор — это объект, у которого имеется метод .next() . Это не совсем верно, но достаточно близко. На самом деле, большая часть контекстов требует объект, который сгенерирует итератор, когда к нему применяется новая встроенная функция iter() . Для того, чтобы определенный пользователем класс (который имеет необходимый метод .next() ) возвращал итератор, нужно всего лишь обеспечить возврат self методом __iter__() . Примеры ниже пояснят сказанное. Метод .next() может вызвать исключение StopIteration , если итерация логически завершилась.

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

В некоторой степени генераторы похожи на замыкания (closure), о которых шла речь в предыдущих статьях о функциональном программировании. Подобно замыканию, генератор "помнит" состояние своих данных. Но с генератором можно достигнуть большего в том смысле, что он также "помнит" свою позицию в пределах структуры управления потоком данных (что в императивном программировании нечто большее, чем просто значения данных). Продолжения (continuations) по-прежнему более общие конструкции, поскольку они позволяют произвольно перемещаться между кадрами стека, а не всегда возвращаться в контекст вызывающей функции (как делает генератор).

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

"Случайное блуждание"

С целью пояснения, позвольте мне поставить довольно простую задачу, которую можно решить различными способами: как новыми, так и старыми. Предположим, мы хотим получить поток случайных чисел меньших единицы, которые подчиняются обратному ограничению. А именно: мы хотим, чтобы каждое следующее число было по крайней мере на 0.4 больше или меньше предыдущего. Более того, сам поток не бесконечен, а заканчивается после случайного числа шагов. Например, мы прервем его, как только появится число меньшее 0.1. Описанные ограничения несколько схожи с теми, что можно найти в алгоритме "случайного блуждания", причем условие окончания напоминает "локальный минимум", но, определенно, эти требования мягче, чем при решении реальных задач.

Python 2.1 или его более ранние версии предлагают несколько методов решения этой задачи. В данном случае будем просто создавать и отправлять список чисел в поток. Это может выглядеть следующим образом:

Однако данный подход обладает ощутимыми ограничениями. Крайне маловероятно, что данный пример сгенерирует длинный список; но, сделав условие прерывания более жестким, мы могли бы создавать произвольно длинные потоки (их точный размер будет случайным, но порядок величин можно спрогнозировать). В определенный момент проблемы памяти и эффективности могут сделать этот подход неприемлемым и излишним. Именно эта проблема и вынудила добавить функции xrange() и xreadlines() в более ранние версии Python. Еще более существенно то, что многие потоки зависят от внешних событий, и все же они должны быть обработаны, когда каждый элемент доступен. Например, поток может прослушивать порт или ожидать ввода пользователя. В этих случаях создание полного списка из такого потока просто неприемлемо. Python 2.1 и более ранние версии предлагали еще один прием: можно было использовать "статическую" локальную переменную для запоминания информации о последнем вызове функции. Очевидно, глобальные переменные могли бы сделать то же самое, но они порождают хорошо знакомые проблемы: засоряют глобальное пространство имен и допускают ошибки, вызванные нелокальностью. Возможно, это вас удивит — если вы не знакомы с этой хитростью — в Python нет "официального" объявления статической области. Однако, если именованные параметры имеют изменяемые значения по умолчанию, они могут быть долговременными хранилищами предыдущих вызовов. Списки, в частности, удобные изменяемые объекты, которые могут содержать даже множественные значения.

Используя "статический" подход мы можем написать следующую функцию:

Эта функция весьма некритична к памяти. Ей достаточно помнить только одно предыдущее значение, она возвращает всего лишь единственное число (а не длинный список чисел). Подобная функция могла бы вернуть следующую величину, зависящую (частично или полностью) от внешних событий. Недостаток этого подхода в том, что он несколько менее лаконичен и много менее элегантен:

Новый способ "блуждания"

"Под капотом" Python 2.2 все последовательности — итераторы. Знаменитая конструкция Python — for elem in lst: — теперь фактически запрашивает lst для создания итератора. Цикл for будет затем последовательно вызывать метод .next() этого итератора, пока не достигнет исключения StopIteration . К счастью, программистам на Python не нужно знать, что происходит в этом месте, поскольку все встроенные типы генерируют свои итераторы автоматически. На самом деле, сейчас словари имеют методы .iterkeys() , .iteritems() и .itervalues() для создания итераторов; первый соответствует новой конструкции: for key in dct: . Подобно этому, новая конструкция for line in file: поддерживается итератором, вызывающим .readline() .

Но зная, что фактически происходит внутри интерпретатора Python, становится очевидным, как использовать пользовательские классы, которые генерируют свои собственные итераторы, а не итераторы встроенных типов. Пример пользовательского класса, позволяющего напрямую использовать randomwalk_list() , а также экономный — поэлеметный — randomwalk_static , приведен ниже:

Применение этого пользовательского итератора аналогично использованию истинного списка, сгенерированного функцией:

На самом деле, выполняется даже конструкция if elem in iterator , которая проверяет столько элементов итератора, сколько нужно для определения истинности (конечно, если она выдаст "ложь", ей потребуется проверить все элементы).

Оставляя след из крошек

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

Генераторы просто обходят всю эту проблему. Генератор "возвращает управление" новым ключевым словом yield , но "помнит" точную точку исполнения, где произошел возврат. При следующем вызове генератора, он начинается с того места, где его оставили до этого, — и в смысле хода исполнения функции, и в смысле значения переменных.

В Python 2.2 генераторы не пишутся непосредственно. Вместо этого пишется функция, возвращающая генератор при вызове. Это может показаться странным, но, поскольку "фабрика функций" является естественной возможностью Python, "фабрика генераторов" кажется ее очевидным концептуальным развитием. Благодаря наличию в теле функции одной или нескольких директив yield , она превращается в "фабрику функций". Если в теле кода встречается директива yield , оператор return может встречаться только без возвращаемого значения. Однако лучше составить тело функции так, чтобы исполнение просто "отваливалось с конца" после того, как все директивы yield будут выполнены. Но если встречается оператор return , то он заставляет созданный генератор вызвать исключение StopIteration , а не вернуть дальнейшие значения.

Мне кажется, что выбор подобного синтаксиса для создания "фабрик генераторов" не совсем оправдан. Директива yield легко может оказаться глубоко в теле функции, и в пределах первых N строк функции будет невозможно определить, является ли функция "фабрикой генераторов". Разумеется, это справедливо и в отношении "фабрики функций", но реализация "фабрики функций" не меняет существующий синтаксис тела функции (и допускается, что иногда тело функции может вернуть простую величину; хотя, возможно, не от хорошей структуры). По-моему, новое ключевое слово — generator вместо def — было бы лучшим выбором.

Оставив в стороне полемику о лучшем синтаксисе, отметим, что генераторы могут работать автоматически как итераторы, когда их для этого вызывают. Для этого не требуется никаких методов классов вроде .__iter__() . Каждая директива yield становится возвращаемой величиной для метода .next() генератора. Простейший пример поясняет сказанное:

Давайте задействуем генератор для решения обсуждавшийся выше задачи:

Простота этого определения более чем привлекательна. Можно использовать этот генератор вручную, либо в качестве итератора. В первом случае генератор может передаваться в пределах программы и вызываться там, где он нужен, и тогда, когда это требуется (что весьма гибко). Простой пример ниже реализует этот случай:

Однако, скорее всего генератор гораздо чаще будет использоваться в качестве итератора, что более компактно (и выглядит как старая добрая последовательность):

Выводы

Потребуется некоторое время, чтобы разработчики, пишущие на Python, освоились со всеми хитростями при использовании генераторов. Мощь, присущая этой простой конструкции, поразительна; я подозреваю, что даже опытные программисты (как разработчики самого 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(итератор[,значение по умолчанию]) . Она автоматически вызывается интерпретатором Python в циклах while и for.

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

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

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

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

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

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

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

  • Первый использует обычный return, он читает все строки файла и заносит их в список, а затем выводит все строки в консоли.
  • Второй использует 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. Оно добавлено в язык, чтобы оптимизировать производительность программы, упростить код и его отладку и дать программистам возможность применять необычные решения в специализированных проектах.

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

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

Отличия генераторов и списков.

Давайте на практике разберемся в отличии списков от генераторов. Для начала создадим список: Как и ожидается тип переменной simple_list является список и перебор элементов выводит ожидаемый результат.

Давайте теперь выполним ту же операцию, но теперь используем генератор: Обратите внимание на то, что при создании генератора используются круглые скобки вместо квадратных. Переменная simple generator имеет уже другой тип, а именно generator . Как вы видите перебор элементов в цикле дает такой же результат как и в предыдущем примере и тут у вас может возникнуть вопрос: так в чем же отличия генератора от обычного списка? Одно из главных их отличий в способе хранения элементов в памяти. Списки хранят сразу все элементы в памяти, в то время как генераторы создают свои элементы на лету в процессе итерации, отображая элемент и удаляя его при переходе к следующему. Это приводит к тому, что для повторного использования генератора его необходимо инициализировать снова.

Использование ключевого слова yield .

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

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

Для того что бы запустить функцию-генератор на исполнения и получить результат её работы необходимо использовать встроенный метод генератора next . Данный метод возвращает первый элемент генератора, а при последующем использовании переходит к следующему элементу. Используемый метод будет возвращать значения пока не будет достигнут последнее значение в генераторе. При достижении конца генератора метод next возвращает ошибку StopIteration :

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

Для получения данных из функции-генератора вместо постоянного использования метода next можно использовать цикл for для итерации по её значениям.

Оптимизация производительности.

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

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

С начала рассмотрим вариант без использования генератора. Создадим функцию которая возвращает список содержащий 1000000 элементов и вычислим используемую память до и после ее вызова. Для вычисления используемой памяти необходимо установить пакет psutil используемый для получения информации о запущенных процессах и использовании системы, это можно сделать воспользовавшись командой pip install psutil .

В результате выполнения данного кода я получил следующий результат(ваш может выглядеть иначе):

До вызова функции использовалось 13 MB памяти, а после создания списка из 1000000 элементов занимаемая память выросла до 196 MB. При этом время необходимое на выполнение операции составило 13,6 секунды.

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

А вот какие результаты я получил на своем компьютере при выполнении этого кода:

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

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

Ссылка на основную публикацию
Что делать если виснет браузер
Автор Юрий Белоусов · 18.03.2019 Пользователи могут столкнуться с неприятной ситуацией, когда браузер Опера зависает, виснет, подвисает, тормозит, лагает, глючит....
Хранение машины в гараже плюсы и минусы
От того, в каких условиях хранится автомобиль, во многом зависит его техническое состояние, а также внешний вид, а при желании...
Хранилище игр на пк
Играй в любимые игры на любом компьютере без лагов и тормозов Играй в крутые игры Как работает Loudplay Мы предоставляем...
Что делать если винда 10 не запускается
В нашей сегодняшней статье будет рассмотрен ряд случаев, связанных с отказом запуска операционной системы Windows 10 на компьютере или ноутбуке....
Adblock detector