Главная > общие вопросы, Программирование > Python: Объяснение работы yield, итераторов и генераторов.

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

Основной источник для этого поста - вопросы и ответы на stackoverflow.

Для понимания что делает "yield", вы должны понимать что такое генераторы. Для понимания что такое генераторы - должны знать об итераторах и итерируемых объектах.

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

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

Когда вы создаёте список (list) вы можете считывать его элементы по одному - это называется итерацией.

>>> lst = [1, 2, 3]
>>> for i in lst:
... print(i)
1
2
3

Lst - итерируемый объект (iterable). Когда вы используете списковые выражения (list comprehensions), вы создаёте список - итерируемый объект:

>>> lst = [x*x for x in xrange(3)]
>>> for i in lst:
... print(i)
0
1
4

Любое объект который вы можете использовать в конструкции "for ... in ..." является итерирумым: списки, строки, файловые объекты и т.п.. Итерирумые объекты достаточно удобны потому что вы можете считывать из них столько данных, сколько вам необходимо, но при этом вы храните все значения последовательности в памяти и это не всегда приемлемо, особенно если вы имеете достаточно большие последовательности.

Генераторы

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

>>> generator = (x*x for x in xrange(3))
>>> for i in generator:
... print(i)
0
1
4

Код выглядит почти так же, как и в предыдущем примере, только вместо квадратных скобочек ("[<...>]") были использованы круглые ("(<...>)"). Заметьте, что вы не можете выполнить цикл по generator во второй раз, поскольку ничего в памяти не хранится, попытка пройтись второй раз будет просто проигнорирована, т.к. generator выбросит при первом запросе на получение следующего значения StopIterationError, однако, вы это не заметите, если будете использовать цикл for, это исключение будет перехвачено и интерпретировано как конец цикла). Но вручную это можно проверить:

>>> generator.next()
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-6-4d83b5efa530> in <module>()
----> 1 generator.next()

StopIteration:

next - это метод для получения следующего значения генератора, если вы его используете не в цикле for.

Yield

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

>>> def generator():
... for i in (1, 2, 3):
... yield i
...
>>> g = generator() # create a generator
>>> print(g)
<generator object generator at 0x2e58870>
>>> for i in g:
... print(i)
1
2
3

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

Для того чтобы до конца освоить оператор yield, вы должны знать, что когда вы вызываете функцию, в теле которой находится yield, выполнение этой функции не происходит. Вместо выполнения, функция вернёт объект-генератор. Выглядит это несколько странно на первый взгляд - функция вызвана, но код не выполнен, но, просто запомните этот факт. Код будет выполнятся при каждой итерации - будь то цикл "for <...> in <generator>" или вызов метода <generator>.next().

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

Управление исполнением генератора

>>> class Bank(object):  # создадим банк, который строит банкоматы (ATM)
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> bank = Bank() # когда всё в порядке, банкоматы выдают деньги
>>> atm = bank.create_atm()
>>> print(atm.next())
$100
>>> print(atm.next())
$100
>>> print([atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> bank.crisis = True # пришёл кризис, денег больше нет
>>> print(atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = bank.create_atm() # это справедливо даже для новых банкоматов
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> bank.crisis = False # проблема в том, что даже после кризиса в банкоматах нет денег
>>> print(atm.next())
<type 'exceptions.StopIteration'>
>>> new_atm = bank.create_atm() # построим новый банкомат с деньгами
>>> for cash in new_atm:
... print cash
$100
$100
$100
...

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

Модуль стандартной библиотеки python - itertools

Модуль стандартной библиотеки python itertools содержит специальные функции для создания и работы с итераторами. С помощью этого модуля вы можете: клонировать итератор, объединить в цепочку несколько итераторов, сгруппировать значения вложенных списков в один, использовать версию map/zip на генераторах - imap/izip, на этом список не заканчивается.

Например, давайте вычислим все возможные варианты прихода лошадей в скачках (задача из комбинаторики - перестановки без повторов элементов):

>>> horses = [1, 2, 3]
>>> races = itertools.permutations(horses)
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

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

Понимание внутренней механики итерации

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

Более подробные объяснения будут рассмотрены в дальнейших статьях и переводах.

Пожалуйста, оцените полезность и качество данной статьи. Одна звезда - плохо, 5 - хорошо.
1/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.2/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.3/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.4/5.5/5. (8 голосов, средний: 3,88 из 5)
Загрузка...
  1. Ishayahu
    7 мая 2013 в 07:11 | #1

    Версия 3.3

    >>> class Bank():
    crisis = False
    def create_atm(self):
    while not self.crisis:
    yield "$100"

    >>> bank=Bank()
    >>> atm=bank.create_atm()
    >>> bank.next()
    Traceback (most recent call last):
    File "", line 1, in
    bank.next()
    AttributeError: 'Bank' object has no attribute 'next'
    >>> atm.next()
    Traceback (most recent call last):
    File "", line 1, in
    atm.next()
    AttributeError: 'generator' object has no attribute 'next'
    >>> atm.__next__()
    '$100'
    >>> bank.__next__()
    Traceback (most recent call last):
    File "", line 1, in
    bank.__next__()
    AttributeError: 'Bank' object has no attribute '__next__'

  2. lizz
    7 мая 2013 в 14:10 | #2

    @ Ishayahu
    Спасибо за коммент. Вместо bank должно быть, конечно, atm. Поправил пост.

  3. lizz
    7 мая 2013 в 14:14 | #3

    "Съеденые" html'ом exception'ы тоже восстановил.

  4. PythonNoob
    1 августа 2013 в 14:08 | #4

    Огромное спасибо! Исправьте опечатку во фразе "но они получить вам надо будет" --- лишнее слово "они".

  5. lizz
    5 августа 2013 в 13:18 | #5

    @PythonNoob
    Спасибо. Перефразировал предложение чтобы оно было более точным.

  6. Стас
    24 апреля 2015 в 15:51 | #6

    Здорово! просто и понятно, читаю лутца и бизли, но не как немог собрать в кучу все понятия! только при прочтении понял как работает yield и для чего она. спасибо большое!

  7. Dima
    11 сентября 2016 в 13:40 | #7

    Вместо next() нужно __next__

  8. lizz
    4 января 2017 в 01:23 | #8

    @Dima, правильно говорить "нужно в третьем питоне" ;)

  1. Пока что нет уведомлений.