Вопрос-Ответ

What does the "yield" keyword do in Python?

Что делает ключевое слово "yield" в Python?

Для чего используется yield ключевое слово в Python? Что оно делает?

Например, я пытаюсь понять этот код1:

def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild

И это вызывающий объект:

result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Что происходит при вызове метода _get_child_candidates?
Возвращается ли список? Один элемент? Вызывается ли он снова? Когда прекратятся последующие вызовы?


1. Этот фрагмент кода был написан Йохеном Шульцем (jrschulz), который создал отличную библиотеку Python для метрических пространств. Это ссылка на полный исходный код: Модуль mspace.
Переведено автоматически
Ответ 1

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

Итеративные

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

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

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

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

Все, для чего вы можете использовать "for... in...", является итерируемым; lists, strings, файлы...

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

Генераторы

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

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

Это то же самое, за исключением того, что вы использовали () вместо []. НО вы не можете выполнить for i in mygenerator второй раз, поскольку генераторы можно использовать только один раз: они вычисляют 0, затем забывают об этом и вычисляют 1, и заканчивают после вычисления 4, один за другим.

Yield

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

>>> def create_generator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4

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

Чтобы освоить yield, вы должны понимать, что при вызове функции код, написанный вами в теле функции, не выполняется. Функция возвращает только объект-генератор, это немного сложно.

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

Теперь сложная часть:

В первый раз, когда for вызывает объект генератора, созданный из вашей функции, он будет запускать код в вашей функции с самого начала, пока не достигнет yield , затем он вернет первое значение цикла. Затем каждый последующий вызов будет запускать еще одну итерацию цикла, который вы написали в функции, и возвращать следующее значение. Это будет продолжаться до тех пор, пока генератор не будет считаться пустым, что происходит, когда функция запускается без нажатия yield. Это может быть потому, что цикл подошел к концу, или потому, что вы больше не удовлетворяете "if/else".


Ваш код объяснен

Генератор:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

# Here is the code that will be called each time you use the generator object:

# If there is still a child of the node object on its left
# AND if the distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild

# If there is still a child of the node object on its right
# AND if the distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild

# If the function arrives here, the generator will be considered empty
# There are no more than two values: the left and the right children

Вызывающий:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

# Get the last candidate and remove it from the list
node = candidates.pop()

# Get the distance between obj and the candidate
distance = node._get_dist(obj)

# If the distance is ok, then you can fill in the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)

# Add the children of the candidate to the candidate's list
# so the loop will keep running until it has looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

This code contains several smart parts:


  • The loop iterates on a list, but the list expands while the loop is being iterated. It's a concise way to go through all these nested data even if it's a bit dangerous since you can end up with an infinite loop. In this case, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) exhausts all the values of the generator, but while keeps creating new generator objects which will produce different values from the previous ones since it's not applied on the same node.



  • The extend() method is a list object method that expects an iterable and adds its values to the list.



Usually, we pass a list to it:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

But in your code, it gets a generator, which is good because:


  1. You don't need to read the values twice.

  2. You may have a lot of children and you don't want them all stored in memory.

And it works because Python does not care if the argument of a method is a list or not. Python expects iterables so it will work with strings, lists, tuples, and generators! This is called duck typing and is one of the reasons why Python is so cool. But this is another story, for another question...

You can stop here, or read a little bit to see an advanced use of a generator:

Controlling a generator exhaustion

>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Примечание: Для Python 3 используйтеprint(corner_street_atm.__next__()) или print(next(corner_street_atm))

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

Itertools, ваш лучший друг

Модуль itertools содержит специальные функции для управления итеративными объектами. Когда-нибудь хотели продублировать генератор? Связать два генератора? Сгруппировать значения во вложенный список с однострочником? Map / Zip без создания другого списка?

Тогда просто import itertools.

Пример? Давайте посмотрим возможные порядки прибытия для забега на четырех лошадях:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]

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

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

Подробнее об этом в этой статье о том, как for циклы.

Ответ 2

Краткий путь к пониманию yield

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


  1. Вставьте строку result = [] в начале функции.

  2. Замените каждое из них yield expr на result.append(expr).

  3. Вставьте строку return result в нижней части функции.

  4. Ура - больше никаких yield инструкций! Прочитайте и разберитесь в коде.

  5. Сравните функцию с исходным определением.

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

Не путайте свои итераторы, итераторы и генераторы

Во-первых, протокол iterator - когда вы пишете

for x in mylist:
...loop body...

Python выполняет следующие два шага:


  1. Получает итератор для mylist:


    Вызов iter(mylist) -> возвращает объект с помощью next() метода (или __next__() в Python 3).


    [Это шаг, о котором большинство людей забывают вам рассказать]



  2. Использует итератор для перебора элементов:


    Продолжайте вызывать next() метод в итераторе, возвращаемом с шага 1. Возвращаемое значение из next() присваивается x и тело цикла выполняется. Если исключение StopIteration возникает изнутри next(), это означает, что в итераторе больше нет значений и цикл завершен.



Правда в том, что Python выполняет два вышеуказанных шага всякий раз, когда хочет перебрать содержимое объекта - так что это может быть цикл for , но это также может быть код типа otherlist.extend(mylist) (где otherlist - список Python).

Здесь mylist является итерируемым, потому что оно реализует протокол iterator. В пользовательском классе вы можете реализовать __iter__() метод, чтобы сделать экземпляры вашего класса итеративными. Этот метод должен возвращать итератор. Итератор - это объект с next() методом. Можно реализовать оба __iter__() и next() в одном классе и иметь __iter__() return self. Это сработает для простых случаев, но не тогда, когда вы хотите, чтобы два итератора выполняли цикл над одним и тем же объектом одновременно.

Итак, это протокол iterator, многие объекты реализуют этот протокол:


  1. Встроенные списки, словари, кортежи, наборы и файлы.

  2. Пользовательские классы, реализующие __iter__().

  3. Генераторы.

Обратите внимание, что for цикл не знает, с каким объектом он имеет дело - он просто следует протоколу iterator и с удовольствием получает элемент за элементом по мере вызова next(). Встроенные списки возвращают свои элементы один за другим, словари возвращают ключи один за другим, файлы возвращают строки одну за другой и т.д. И генераторы возвращают... ну, вот тут-то и пригодится yield:

def f123():
yield 1
yield 2
yield 3

for item in f123():
print item

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

So the generator object is sort of like an adapter - at one end it exhibits the iterator protocol, by exposing __iter__() and next() methods to keep the for loop happy. At the other end, however, it runs the function just enough to get the next value out of it and puts it back in suspended mode.

Why use generators?

Обычно вы можете написать код, который не использует генераторы, но реализует ту же логику. Один из вариантов - использовать "трюк" с временным списком, о котором я упоминал ранее. Это будет работать не во всех случаях, например, если у вас бесконечные циклы, или это может привести к неэффективному использованию памяти, когда у вас действительно длинный список. Другой подход заключается в реализации нового итерируемого класса, SomethingIter который сохраняет состояние в элементах экземпляра и выполняет следующий логический шаг в своем методе next() (или __next__() в Python 3). В зависимости от логики код внутри next() метода может в конечном итоге выглядеть очень сложным и подверженным ошибкам. Здесь генераторы предоставляют чистое и простое решение.

Ответ 3

Подумайте об этом так:

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

Оригинальная версия:

def some_function():
for i in xrange(4):
yield i

for i in some_function():
print i

Это в основном то, что интерпретатор Python делает с приведенным выше кодом:

class it:
def __init__(self):
# Start at -1 so that we get 0 when we add 1 below.
self.count = -1

# The __iter__ method will be called once by the 'for' loop.
# The rest of the magic happens on the object returned by this method.
# In this case it is the object itself.
def __iter__(self):
return self

# The next method will be called repeatedly by the 'for' loop
# until it raises StopIteration.
def next(self):
self.count += 1
if self.count < 4:
return self.count
else:
# A StopIteration exception is raised
# to signal that the iterator is done.
# This is caught implicitly by the 'for' loop.
raise StopIteration

def some_func():
return it()

for i in some_func():
print i

Для получения дополнительной информации о том, что происходит за кулисами, for цикл можно переписать в это:

iterator = some_func()
try:
while 1:
print iterator.next()
except StopIteration:
pass

Это имеет больше смысла или просто еще больше сбивает вас с толку? :)

Я должен отметить, что это является чрезмерным упрощением в иллюстративных целях. :)

Ответ 4

yield Ключевое слово сводится к двум простым фактам:


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

  2. Генератор является итеративным. Что такое итерируемый? Это что-то вроде list, set, range словарного представления или любого другого объекта со встроенным протоколом для посещения каждого элемента в определенном порядке.

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

generator = myYieldingFunction(...)  # basically a list (but lazy)
x = list(generator) # evaluate every element into a list

generator
v
[x[0], ..., ???]

generator
v
[x[0], x[1], ..., ???]

generator
v
[x[0], x[1], x[2], ..., ???]

StopIteration exception
[x[0], x[1], x[2]] done

В принципе, всякий раз, когда встречается оператор yield, функция приостанавливается и сохраняет свое состояние, затем выдает "следующее возвращаемое значение в "списке"" в соответствии с протоколом итератора python (в некоторую синтаксическую конструкцию, такую как цикл for, который повторно вызывает next() и улавливает StopIteration исключение и т.д.). Возможно, вы сталкивались с генераторами с выражениями генератора; функции генератора более мощные, потому что вы можете передавать аргументы обратно в приостановленную функцию генератора, используя их для реализации сопрограмм. Подробнее об этом позже.


Базовый пример ('список')

Давайте определим функцию, makeRange похожую на Python range. Вызов makeRange(n) ВОЗВРАЩАЕТ ГЕНЕРАТОР:

def makeRange(n):
# return 0,1,2,...,n-1
i = 0
while i < n:
yield i
i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

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

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Сравнение примера с "просто возвращением списка"

Приведенный выше пример можно рассматривать как простое создание списка, к которому вы добавляете и возвращаете:

# return a list                  #  # return a generator
def makeRange(n): # def makeRange(n):
"""return [0,1,2,...,n-1]""" # """return 0,1,2,...,n-1"""
TO_RETURN = [] #
i = 0 # i = 0
while i < n: # while i < n:
TO_RETURN += [i] # yield i
i += 1 # i += 1
return TO_RETURN #

>>> makeRange(5)
[0, 1, 2, 3, 4]

Однако есть одно существенное отличие; смотрите Последний раздел.


Как можно использовать генераторы

Итеративность - это последняя часть понимания списка, и все генераторы итеративны, поэтому их часто используют следующим образом:

#                  < ITERABLE >
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Чтобы лучше разобраться в генераторах, вы можете поиграть с itertools модулем (обязательно используйте chain.from_iterable, а не chain, когда это необходимо). Например, вы могли бы даже использовать генераторы для реализации бесконечно длинных отложенных списков, таких как itertools.count(). Вы могли бы реализовать свое собственное def enumerate(iterable): zip(count(), iterable) или, альтернативно, сделать это с yield ключевым словом в цикле while.

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


За кулисами

Вот как работает "протокол итерации Python". То есть что происходит, когда вы это делаете list(makeRange(5)). Это то, что я описал ранее как "ленивый, инкрементный список".

>>> x=iter(range(5))
>>> next(x) # calls x.__next__(); x.next() is deprecated
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

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


Сопрограммы

Пример сопрограммы:

def interactiveProcedure():
userResponse = yield makeQuestionWebpage()
print('user response:', userResponse)
yield 'success'

coroutine = interactiveProcedure()
webFormData = next(coroutine) # same as .send(None)
userResponse = serveWebForm(webFormData)

# ...at some point later on web form submit...

successStatus = coroutine.send(userResponse)

Сопрограмма (генераторы, которые обычно принимают входные данные через yield ключевое слово, например, nextInput = yield nextOutput, как форма двусторонней связи) - это, по сути, вычисление, которому разрешено приостанавливать себя и запрашивать входные данные (например, о том, что оно должно делать дальше). Когда сопрограмма приостанавливает саму себя (когда запущенная сопрограмма в конечном итоге достигает yield ключевого слова), вычисление приостанавливается, а управление инвертируется (передается) обратно "вызывающей" функции (фрейму, который запросил next значение вычисления). Приостановленный генератор / сопрограмма остается приостановленным до тех пор, пока другая вызывающая функция (возможно, другая функция / контекст) не запросит следующее значение, чтобы отменить приостановку (обычно передавая входные данные, чтобы направить приостановленную внутреннюю логику в код сопрограммы).

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


Подробности

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

На языке Python, iterable - это любой объект, который "понимает концепцию цикла for", например список [1,2,3], а итератор - это конкретный экземпляр запрошенного цикла for, подобный [1,2,3].__iter__(). Генератор точно такой же, как любой итератор, за исключением того, как он был написан (с синтаксисом функции).

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

Таким образом, в маловероятном случае, если вам не удастся сделать что-то подобное...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... тогда помните, что генератор - это итератор; то есть он используется один раз. Если вы хотите использовать его повторно, вам следует вызвать myRange(...) еще раз. Если вам нужно использовать результат дважды, преобразуйте результат в список и сохраните его в переменной x = list(myRange(5)). Те, кому абсолютно необходимо клонировать генератор (например, кто занимается ужасающе хакерским метапрограммированием), могут использовать itertools.tee (все еще работает в Python 3), если это абсолютно необходимо, поскольку предложение по стандартам Python PEP с копируемым итератором было отложено.

python