т. (383) 381-86-26

Блог о создании вебсайтов

 

Шпаргалка для понимания работы yield в Python

 
Этот пост входит в цикл на основе переводов популярных вопросов/ответов со stackoverflow.

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

  • 1. Вначале функции добавь строку: result = [].
  • 2. Замени каждый вызов yield на result.append(expr).
  • 3. Добавь return result в конец определения функции.
  • 4. Ура! Теперь нет ни одного вызова yield — теперь легко понять что делает функция.
  • 5/ Разобрался? Верни определение функции в первоначальное состояние.

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

Различай итерируемые объекты, итераторы и генераторы

Для начала разберемся с интерфейсом итератора. Когда ты пишешь:

>>> for x in mylist:
>>> .... тело цикла ....

Python совершает два следующих шага:

  1. Получает объект итератора для списка mylist.

    Вызов iter(mylist) возвращает объект с методом next()(или __next__() для Py3).

    >>> iter([1,2,3])
    >>> <listiterator object at 0xb72b0e0c>
                  

  2. Использует полученный итератор для того, чтобы пройти в цикле по элементам списка. Затем for x in mylist: ... последовательно вызывает метод .next(), полученного на первом шаге итератора, и значение, которое возвращает .next() присваивается x. Если список закончился, то .next() генерирует исключение StopIteration.

На самом деле Python проделывает описанные шаги всякий раз, когда ему требуется пройтись последовательно по содержимому объекта: как с помощью цикла for, так и в коде вида therlist.extend(mylist)

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

Наш mylist — это итерируемый объект, который реализует протокол итератора. Если тебе потребуется в каком-либо твоем классе сделать его экземпляры итерируемыми, то для этого потребуется всего лишь реализовать в нём метод __iter__(). Этот метод должен возвращать итератор. Итератор — это объект с методом .next().

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

В целом, это и есть протокол итератора. Многие объекты Python его реализуют:

  1. Встроенные списки, словари, кортежи, множества, файлы
  2. Пользовательские классы, реализующие метод __iter__()
  3. Генераторы

Цикл for не знает с каким объектом взаимодействует. Цикл просто следует протоколу итератора и счастливо берёт элемент за элементом с каждым новым вызовом .next(). Списки возвращают последовательно элемент за элементом, словари отдают последовательно ключи, файлы — строка за строкой. А генераторы... что ж это то место, где вступает в игру yield.

>>> # определим функцию
>>> def f123():
>>>     yield 1
>>>     yield 2
>>>     yield 3
>>> for item in f123():
>>>     print item
>>> 1
>>> 2
>>> 3

Если бы я написал вместо yield три раза return, то тогда при вызове f123() сработал бы первый встреченный return и функция завершила бы свою работу. Однако, так как мы использовали yield, то функция становится неординарной и когда интерпретатор достигает yield, то возвращается объект генератор (а не вычисляемое значение) — затем функция переходит в приостановленное состояние. Когда, к примеру, когда в цикл for передаётся объект генератора, то f123() возобновляет работу, отрабатывает до следующего встреченного yield и возвращает следующее значение. Это происходит до тех пор, пока цикл не завершает свою работу в следствии того, что генератор выдает исключение StopIteration.

Таким образом объект генератора выглядит как некоторый адаптер: с одной стороны он реализует протокол итератора, определяя методы __iter__() и next(), с другой стороны он исполняет функцию ровно на столько сколько требуется для получения очередного значения, а затем снова приостанавливает её.

Зачем использовать генераторы

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

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

Читайте также

Yield — что это за слово такое?

Подпишитесь на рассылку, будет интересно!