Что такое yield в python

Что такое yield и как его использовать в Python? Loftblog на PiterPy

27.04.15 – 901 3:34
181 день – 4 539 1:00:44
333 дня – 2 326 19:22

Интервью с Александром Кошкиным — программистом из Positive Technologies.
Александр рассказал о компании Positive Technologies, чем они занимаются, почему они называются "добрыми хакерами". Еще он объяснил что такое генераторы и что они из себя представляют, плюсы и минусы языка программирования Python.

Портал видеоуроков: loftblog.ru
Школа онлайн обучения: loftschool.com
Фильтр новостей в мире информационных технологий: vk.com/loftblog
Также в facebook: facebook.com/loftblog
И, конечно, twitter: twitter.com/loft_blog
—————————————-­­­­————————————-­-­-­-­—
Не забываем, что самый лучший способ сказать "спасибо" — нажать кнопку "нравится" и скинуть ссылку на интервью друзьям. Ничто другое так сильно не мотивирует авторов продолжать работу 🙂

2 дня – 66 390 9:33

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

Как использовать 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: генераторы, стримы и yield

Python

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

А ещё изучим две другие концепции из информатики: ленивые (отложенные) вычисления и потоки данных (стримы).

Итерируемые объекты

Для начала узнаем, что такое итерируемый объект, а затем разберёмся, как используется generator — в сущности это тоже итератор.

В Python итерируемый объект — это объект, над которым производятся так называемые проходы (итерации). Например, как в цикле for .

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

Так же мы можем проитерировать и символы в строке.

Ограничение итераций

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

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

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

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

Генератор

generator — тоже итератор, но его ключевое свойство — ленивые вычисления. Это классическая концепция в информатике, и её переняли многие языки программирования, такие как Haskell. Основная идея этой концепции звучит как вызов-по-необходимости. Отложенные вычисления могут приводить к снижению доступной процессу памяти.

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

Доступно два способа создания generator : выражение генератора и функция генератора.

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

Разница тут в том, что мы не вычисляем все значения при создании generator . x*x вычисляется тогда, когда мы итерируем generator .

Чтобы понять разницу, давайте запустим сниппет кода.

Как можем видеть из результата, когда мы создаём итерируемый объект, вычисление занимает 10 секунд, потому что мы извлекаем time.sleep(1) 10 раз.

Но в реальности, когда мы создаём generator , time.sleep(1) не выполняется.

Yield

Другой способ создать generator — использовать функцию генератора. Мы берём ключевое слово yield , чтобы вернуть generator в функции.

Давайте посмотрим, как сработает эта функция на fib , где возвращается generator с n числами Фибоначчи.

Давайте применим yield , чтобы переписать программу чтения файла, приведённую выше.

С таким подходом мы не будем загружать всё содержимое в память. Вместо этого мы загрузим его путём чтения строк.

Поток данных

С генератором мы создадим структуру данных с бесконечным количеством элементов. Этот вид последовательности элементов данных называется в информатике потоком данных (или “стрим”). С его помощью мы можем выражать концепции бесконечных последовательностей математическими методами.

Например, нам нужна последовательность со всеми числами Фибоначчи. Как мы её получим?

Нам всего-то нужно убрать параметр счётчика из функции выше.

Вуаля! Мы получаем переменную, которая могла бы отражать все числа Фибоначчи. Давайте напишем общую функцию, чтобы взять n элементов из любого потока.

Выражение take(all_fib_numbers, 10) будет в результате возвращать первые 10 чисел Фибоначчи.

Заключение

generator в языке Python — это мощный инструмент для отложенных вычислений, экономии памяти и времени.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *