т. (383) 381-86-26

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

 

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

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

Когда я впервые познакомился с Python (а это было после Паскаля, С, Дельфи и Лисп -- примерно в таком порядке попадались мне языки), то он показался бредовым: фигурных скобок нет, блоки отделяются друг от друга пробелами, типы переменных указывать не нужно, да и вообще не нужно определять ничего и т.д. Но недоумение быстро сменилось на удивление, а удивление сменилось на радость. Оказалось, что писать на Питоне (и да пусть сдохнут все урюки, которые сейчас будут исправлять меня на "Пайтон" -- я говорю и пишу Питон, так как это моё личное эстетическое предпочтение, да и переводится дословно оно также) всё равно, что писать на русском языке: приятно, легко, язык имеет богатые выразительные возможности, подкрепленные силовыми батарейками в виде стандартной библиотеки и множеством батареек сторонних производителей. В общем язык я полюбил всерьез и надолго.

Однако, как и в русском языке за многими простыми вещами скрываются многочисленные продвинутые понятия, которые не сразу поддаются раскусыванию. Одним из таких орешков является yield. "для чего нужен yield?", "что делает yield?" — пожалуй чуть ли не самые частые вопросы.

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

Итераторы

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

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

А сами списки являются итерируемыми объектами. Кстати, списковые выражения (list comprehensions) создают списки, а значит они также являются итерируемыми:

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

Все знакомые тебе объекты, для которых ты можешь использовать конструкцию for ... in ... являются итерируемыми объектами. Такие объекты очень удобны, так как ты можешь читать их значения последовательно в цикле столько раз, сколько потребуется. Но есть один минус: все значения итератора хранятся в памяти. Это значит, что если список или кортеж содержит пару миллиардов записей, то память будет выделена под весь миллиард записей. А это уже не очень удобно, не всегда нужно, а иногда и невозможно в силу каких-нибудь технических ограничений системы.

Генераторы

Генераторы — это подмножество итераторов. Разница заключается в том, что с помощью for ... in ... ты можешь пройти по генератору только один раз, так как значения генератора не хранятся в памяти, они генерируются в процессе итерирования, т.е. можно сказать, что каждое следующее значение в генераторе вычисляется на лету.

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

Пример аналогичен предыдущему, за исключением того, что вместо спискового выражения используется выражение-генератор. mygenerator также как и mylist можно итерировать, но только один раз. Так как после того, как вычислен 0 генератор о нём забывает и вычисляет 1; после забывает единицу и вычисляет 4 и т.д. В этом природа генератора — вычисление на лету. Очень, кстати, экономит память. Генератор определяется двумя способами: с помощью выражения-генератора и при помощи зарезервированного слова yield.

Yield

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

>>> def createGenerator():
       ...    mylist = range(3)
       ...    for i in mylist:
       ...        yield i*i

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

>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>

Код в теле функции будет вычисляться, когда ты передашь генератор, к примеру в цикл:

>>> for i in mygenerator:
        ...     print(i)
0
1

Либо вручную вызовешь метод next()

>>> mygenerator.next()
0
>>> mygenerator.next()
1

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

Конец процесса работы генератора наступает тогда, когда генератор считается пустым. Т.е. очередной вызов вычисления определенной нами функции методом next() не добрался до yield по причине того, что либо цикл закончился, либо не были удовлетворены какие-либо другие услвоия для продолжения работы генератора. В этом случае генератор выкидывает исключение StopIteration

Заключение

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

Для более глубокого проникновения в тему следует прочитать:

Так же будет полезно ознакомиться со шпаргалкой по yield

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

Шпаргалка по использованию yield в Python

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