Что делает ключевое слово yield? - PullRequest
9261 голосов
/ 24 октября 2008

Какая польза от ключевого слова 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 .

Ответы [ 38 ]

13434 голосов
/ 24 октября 2008

Чтобы понять, что делает 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 - это ключевое слово, которое используется как return, за исключением того, что функция вернет генератор.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator 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 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 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 is no more than two values: the left and the right children

Caller:

# 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 distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidates list
    # so the loop will keep running until it will have 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

Этот код содержит несколько умных частей:

  • Цикл повторяется в списке, но список расширяется во время итерации цикла :-) Это краткий способ просмотреть все эти вложенные данные, даже если это немного опасно, так как вы можете получить бесконечный цикл. В этом случае candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) исчерпывает все значения генератора, но while продолжает создавать новые объекты генератора, которые будут генерировать значения, отличные от предыдущих, поскольку он не применяется к одному узлу.

  • Метод extend() - это метод объекта списка, который ожидает итерацию и добавляет ее значения в список.

Обычно мы передаем ему список:

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

Но в вашем коде он получает генератор, что хорошо, потому что:

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

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

Вы можете остановиться здесь или прочитать немного, чтобы увидеть расширенное использование генератора:

Контроль истощения генератора

>>> 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 циклы .

1796 голосов
/ 26 октября 2008

Ярлык для понимания yield

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

  1. Вставить строку result = [] в начале функции.
  2. Заменить каждый yield expr на result.append(expr).
  3. Вставьте строку return result в нижней части функции.
  4. Yay - не более yield заявлений! Прочитайте и выясните код.
  5. Сравнить функцию с исходным определением.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Обратите внимание, что цикл for не знает, с каким объектом он имеет дело - он просто следует протоколу итератора и рад получить элемент за элементом, так как он вызывает 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 и цикл завершается.

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

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

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

469 голосов
/ 24 октября 2008

Думайте об этом так:

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

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

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

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

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

397 голосов
/ 19 июня 2011

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

  1. Если компилятор обнаруживает ключевое слово yield в любом месте внутри функции, эта функция больше не возвращается через оператор return. Вместо , немедленно возвращает ленивый объект «отложенного списка» , называемый генератором
  2. Генератор повторяется. Что такое повторяемый ? Это что-то вроде list или set или range или dict-view, со встроенным протоколом для посещения каждого элемента в определенном порядке .

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

generator = myYieldingFunction(...)
x = list(generator)

   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

list==[x[0], x[1], x[2]]

* +1037 * Пример * +1038 *

Давайте определим функцию makeRange, которая похожа на range в Python. Вызов 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]

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

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

# list-version                   #  # generator-version
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  ## indented
    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)
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() просто вызывает функцию .next() объектов, которая является частью "протокола итерации" и встречается на всех итераторах. Вы можете вручную использовать функцию next() (и другие части протокола итерации) для реализации необычных вещей, обычно за счет читабельности, поэтому постарайтесь не делать этого ...


Minutiae

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

В языке Python говорят, что итерируемый - это любой объект, который "понимает концепцию цикла 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 PEP для копирования было отложенный.

316 голосов
/ 25 июня 2015

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

План ответа / резюме

  • Функция с yield, при вызове возвращает Генератор .
  • Генераторы являются итераторами, поскольку они реализуют протокол итераторов , поэтому вы можете выполнять итерации по ним.
  • Генератор также может отправлять информацию , что делает его концептуально сопрограммой .
  • В Python 3 вы можете делегировать от одного генератора к другому в обоих направлениях с помощью yield from.
  • (Приложение критикует пару ответов, включая самый верхний, и обсуждает использование return в генераторе.)

Генераторы

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

Идея для генераторов пришла из других языков (см. Сноску 1) с различными реализациями. В генераторах Python выполнение кода заморожено в точке выхода. Когда вызывается генератор (методы обсуждаются ниже), выполнение возобновляется, а затем останавливается при следующем выходе.

yield обеспечивает простой способ реализации протокола итератора , определяемый следующими двумя методами: __iter__ и next (Python 2) или __next__ (Python 3). Оба эти метода сделать объект итератором, который вы можете проверять с помощью Iterator Abstract Base Класс из collections модуля.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Тип генератора является подтипом итератора:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

А при необходимости мы можем проверить тип так:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Особенностью Iterator является то, что после исчерпания вы не можете повторно использовать или сбросить его:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Вам придется сделать еще один, если вы хотите снова использовать его функциональность (см. Сноску 2):

>>> list(func())
['I am', 'a generator!']

Можно получить данные программно, например:

def func(an_iterable):
    for item in an_iterable:
        yield item

Вышеуказанный простой генератор также эквивалентен приведенному ниже - начиная с Python 3.3 (и недоступен в Python 2), вы можете использовать yield from:

def func(an_iterable):
    yield from an_iterable

Тем не менее, yield from также допускает делегирование субгенераторам, что будет объяснено в следующем разделе о совместном делегировании с суб-сопрограммами.

Сопрограмма:

yield образует выражение, позволяющее отправлять данные в генератор (см. Сноску 3)

Вот пример, обратите внимание на переменную received, которая будет указывать на данные, отправляемые в генератор:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Сначала мы должны поставить генератор в очередь с помощью встроенной функции, next. Будет вызовите соответствующий метод next или __next__, в зависимости от версии Python, который вы используете:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

И теперь мы можем отправлять данные в генератор. ( Отправка None is так же, как звонить next.):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Совместная делегация в суб-сопрограмме с yield from

Теперь напомним, что yield from доступен в Python 3. Это позволяет нам делегировать сопрограммы на подкупу:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

И теперь мы можем делегировать функциональность суб-генератору, и его можно использовать генератором, как указано выше:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Вы можете узнать больше о точной семантике yield from в PEP 380.

Другие методы: закрыть и бросить

Метод close вызывает GeneratorExit в точке функции казнь была заморожена. Это также будет называться __del__, так что вы можно поместить любой код очистки, где вы обрабатываете GeneratorExit:

>>> my_account.close()

Вы также можете выдать исключение, которое может быть обработано в генераторе. или передано обратно пользователю:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Заключение

Мне кажется, я рассмотрел все аспекты следующего вопроса:

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

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


Приложение:

Критика топ / принятого ответа **

  • Это путает то, что делает итерацию 1169 *, просто используя список в качестве примера. См. Мои ссылки выше, но вкратце: итерируемый имеет метод __iter__, возвращающий итератор . Итератор предоставляет метод .next (Python 2 или .__next__ (Python 3), который неявно вызывается циклами for до тех пор, пока он не вызовет StopIteration, и, как только он это сделает, он продолжит сделать это.
  • Затем он использует выражение генератора, чтобы описать, что такое генератор. Поскольку генератор - это просто удобный способ создания итератора , он только запутывает дело, а мы до сих пор не дошли до yield.
  • В Управление исчерпанием генератора он вызывает метод .next, когда вместо этого ему следует использовать встроенную функцию next. Это был бы соответствующий уровень косвенности, потому что его код не работает в Python 3.
  • Itertools? Это не имело отношения к тому, что yield делает вообще.
  • Нет обсуждения методов, которые yield предоставляет наряду с новой функциональностью yield from в Python 3. Верхний / принятый ответ - очень неполный ответ.

Критика ответа с предложением yield в выражении-генераторе или понимании.

В настоящее время грамматика допускает любое выражение в понимании списка.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Так как yield является выражением, некоторые считают, что его интересно использовать в выражениях или выражениях-генераторах, несмотря на то, что он не привел ни одного особенно хорошего варианта использования.

Разработчики ядра CPython обсуждают отказ от его разрешения . Вот соответствующий пост из списка рассылки:

30 января 2017 года в 19:05 Бретт Кэннон написал:

В воскресенье, 29 января 2017 года в 16:39 Крейг Родригес написал:

Я в порядке с любым подходом. Оставить вещи такими, какие они есть в Python 3 не хорошо, ИМХО.

Мой голос - это ошибка SyntaxError, поскольку вы не получаете того, чего ожидаете от синтаксис.

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

Что касается того, как туда добраться, мы, вероятно, захотим:

  • Предупреждение о синтаксисе или предупреждение об устаревании в 3.7
  • Py3k предупреждение в 2.7.x
  • Синтаксическая ошибка в 3.8

Ура, Ник.

- Ник Коглан | ncoghlan на gmail.com | Брисбен, Австралия

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

Итог, пока разработчики CPython не скажут нам иначе: Не помещайте yield в выражение или понимание генератора.

Оператор return в генераторе

В Python 2 :

В функции генератора оператор return не может включать expression_list. В этом контексте пустое значение return указывает, что генератор завершен, и вызовет повышение значения StopIteration.

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

В Python 3 :

В функции генератора оператор return указывает, что генератор завершен и вызовет StopIteration для поднятия. Возвращаемое значение (если есть) используется в качестве аргумента для построения StopIteration и становится атрибутом StopIteration.value.

Сноска

  1. Языки CLU, Sather и Icon были указаны в предложении представить концепцию генераторов в Python. Общая идея что функция может поддерживать внутреннее состояние и давать промежуточные Точки данных по запросу пользователя. Это обещало быть превосходным по производительности к другим подходам, включая потоки Python , которые даже недоступны в некоторых системах.

  2. Это означает, например, что объекты xrange (range в Python 3) не являются Iterator с, хотя они итеративны, потому что их можно использовать повторно. Как и списки, их __iter__ методы возвращают объекты итератора.

  3. yield изначально был представлен как утверждение, означающее, что оно может появляться только в начале строки в блоке кода. Теперь yield создает выражение выхода. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Это изменение было предложено , чтобы позволить пользователю отправлять данные в генератор так же, как можно получить это. Чтобы отправить данные, нужно иметь возможность назначить их чему-либо, и для этого заявление просто не сработает.

271 голосов
/ 24 октября 2008

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

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

list.extend вызывает итератор, пока он не исчерпан. В случае с примером кода, который вы опубликовали, было бы намного проще просто вернуть кортеж и добавить его в список.

207 голосов
/ 24 октября 2008

Есть еще одна вещь, которую стоит упомянуть: функция, которая возвращает результат, на самом деле не должна завершаться. Я написал такой код:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Тогда я могу использовать его в другом коде, как это:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Это действительно помогает упростить некоторые проблемы и облегчает работу с некоторыми вещами.

179 голосов
/ 18 января 2013

Для тех, кто предпочитает минимальный рабочий пример, медитируйте на этом интерактивном сеансе Python:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed
172 голосов
/ 25 марта 2016

TL; DR

Вместо этого:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

сделать это:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

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

Это был мой первый "ага" момент с доходностью.


yield это сладкий способ сказать

построить серию вещей

То же поведение:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Различное поведение:

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

Выход ленивый , это откладывает вычисления. Функция с выходом фактически не выполняется вообще, когда вы ее вызываете. Возвращает объект итератора , который запоминает, где он остановился. Каждый раз, когда вы вызываете next() на итераторе (это происходит в цикле for), выполнение в дюймах вперед до следующего выхода. return вызывает StopIteration и заканчивает серию (это естественный конец цикла for).

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

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Если вам нужно несколько проходов и серия не слишком длинная, просто наберите list():

>>> list(square_yield(4))
[0, 1, 4, 9]

Блестящий выбор слова yield, потому что применяются оба значения :

доходность & mdash; производить или предоставлять (как в сельском хозяйстве)

... укажите следующие данные в серии.

доходность & mdash; уступить или отказаться (как при политической власти)

... прекращать выполнение процессора до тех пор, пока итератор не продвинется.

158 голосов
/ 16 января 2013

Доход дает вам генератор.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

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

Во втором случае bar просто дает вам генератор. Генератор является итеративным - это означает, что вы можете использовать его в цикле for и т. Д., Но к каждому значению можно получить доступ только один раз. Все значения также не сохраняются в памяти одновременно; объект генератора «запоминает», где он находился в цикле в последний раз, когда вы его вызывали - таким образом, если вы используете итеративный подсчет (скажем) до 50 миллиардов, вам не нужно считать до 50 миллиардов всех и запишите 50 миллиардов чисел для подсчета.

Опять же, это довольно надуманный пример, вы, вероятно, использовали бы itertools, если бы вы действительно хотели сосчитать до 50 миллиардов. :)

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

...