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

Каково использование ключевого слова 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. This piece of code was written by Jochen Schulz (jrschulz), who made a great Python library for metric spaces. This is the link to the complete source: Module mspace.

@Booboo - вы правы - система не работает - голосование их не уберет

NeilG 12.02.2021 03:53

@booboo: StackOverflow часто дает гораздо лучшие ответы, чем учебники. «Репутация» и «медали» - это просто набор цифр. Не о чем беспокоиться. Что удивительно, так это то, сколько неоплачиваемой работы выполняют модераторы. На это уходит так много личного времени.

Nav 06.03.2021 12:53
Анализ настроения постов в Twitter с помощью Python, Tweepy и Flair
Анализ настроения постов в Twitter с помощью Python, Tweepy и Flair
Анализ настроения текстовых сообщений может быть настолько сложным или простым, насколько вы его сделаете. Как и в любом ML-проекте, вы можете выбрать...
7 лайфхаков для начинающих Python-программистов
7 лайфхаков для начинающих Python-программистов
В этой статье мы расскажем о хитростях и советах по Python, которые должны быть известны разработчику Python.
Установка Apache Cassandra на Mac OS
Установка Apache Cassandra на Mac OS
Это краткое руководство по установке Apache Cassandra.
Сертификатная программа "Кванты Python": Бэктестер ансамблевых методов на основе ООП
Сертификатная программа "Кванты Python": Бэктестер ансамблевых методов на основе ООП
В одном из недавних постов я рассказал о том, как я использую навыки количественных исследований, которые я совершенствую в рамках программы TPQ...
Создание персонального файлового хранилища
Создание персонального файлового хранилища
Вы когда-нибудь хотели поделиться с кем-то файлом, но он содержал конфиденциальную информацию? Многие думают, что электронная почта безопасна, но это...
Создание приборной панели для анализа данных на GCP - часть I
Создание приборной панели для анализа данных на GCP - часть I
Недавно я столкнулся с интересной бизнес-задачей - визуализацией сбоев в цепочке поставок лекарств, которую могут просматривать врачи и...
11 021
2
2 472 271
37
Перейти к ответу Данный вопрос помечен как решенный

Ответы 37

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

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

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

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

kurosch 24.10.2008 22:11

Он возвращает генератор. Я не особо знаком с Python, но считаю, что это то же самое, что и Блоки итератора C#, если вы знакомы с ними.

Ключевая идея заключается в том, что компилятор / интерпретатор / что-то еще делает некоторые трюки, чтобы вызывающий мог продолжать вызывать next (), и он продолжал возвращать значения - как если бы метод генератора был приостановлен. Теперь очевидно, что вы не можете действительно «приостановить» метод, поэтому компилятор создает конечный автомат, чтобы вы запомнили, где вы сейчас находитесь, как выглядят локальные переменные и т. д. Это намного проще, чем написать итератор самостоятельно.

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

Итератор - это просто причудливый термин для объекта, у которого есть метод 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

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

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

__getitem__ можно было бы определить вместо __iter__. Например: class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i), он напечатает: 0, 10, 20, ..., 90
jfs 25.10.2008 06:03

Я пробовал этот пример в Python 3.6, и если я создам iterator = some_function(), переменная iterator больше не будет иметь функцию с именем next(), а только функцию __next__(). Думал упомянуть об этом.

Peter 06.05.2017 17:37

Где реализация цикла for, которую вы написали, вызывает метод __iter__ для iterator, созданного экземпляра it?

SystematicDisintegration 12.05.2020 01:50

К сожалению, это совершенно неверный ответ. Это не то, что интерпретатор Python делает с генераторами. Он не создает класс, начиная с функции генератора, а реализует __iter__ и __next__. То, что он на самом деле делает под капотом, объясняется в этом посте stackoverflow.com/questions/45723893/…. Цитировать @Raymond Hettinger «генераторы не реализованы внутри, как показано в вашем чистом классе python. Вместо этого они имеют большую часть той же логики, что и обычные функции»

gioxc88 15.10.2020 16:52
Ответ принят как подходящий

Чтобы понять, что делает 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 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 is 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 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 candidate's 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.

yield не такой волшебный, как следует из этого ответа. Когда вы вызываете функцию, содержащую инструкцию yield, вы получаете объект-генератор, но код не запускается. Затем каждый раз, когда вы извлекаете объект из генератора, Python выполняет код в функции, пока не дойдет до оператора yield, затем приостанавливает работу и доставляет объект. Когда вы извлекаете другой объект, Python возобновляет работу сразу после yield и продолжает работу, пока не достигнет другого yield (часто того же самого, но на одну итерацию позже). Это продолжается до тех пор, пока функция не завершится, после чего генератор считается исчерпанным.
Matthias Fripp 24.05.2017 00:41

«Эти итерации удобны ... но вы храните все значения в памяти, и это не всегда то, что вам нужно», либо неверно, либо сбивает с толку. Итератор возвращает итератор после вызова iter () для итератора, и итератор не всегда должен сохранять свои значения в памяти, в зависимости от реализации метода iter, он также может генерировать значения в последовательности по запросу.

picmate 涅 15.02.2018 22:21

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

WoJ 07.05.2020 13:12

Возможно, я ошибаюсь, но генератор - это не итератор, а «вызываемый генератор» - это итератор.

aderchox 08.05.2020 15:06

@MatthiasFripp «Это продолжается до тех пор, пока функция не завершится до конца» - или пока не встретится оператор return. (return разрешен в функции, содержащей yield, при условии, что она не указывает возвращаемое значение.)

alani 06.06.2020 09:03

@alaniwi: хороший момент. Думаю, примерно так же и происходит, когда функция работает сама по себе.

Matthias Fripp 09.06.2020 03:03

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

Jacob Ward 03.12.2020 04:23

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

toxicantidote 09.02.2021 01:52

@ e-satis Большое спасибо за этот блестящий ответ! Это гениально! [+1]

William Martens 03.03.2021 16:58

Вот пример простым языком. Я покажу соответствие между высокоуровневыми человеческими концепциями и низкоуровневыми концепциями Python.

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

  • Я звоню вам и говорю, что мне нужна последовательность чисел, которая создается определенным образом, и я сообщаю вам, каков алгоритм.
    Этот шаг соответствует def, определяющему функцию генератора, то есть функцию, содержащую yield.
  • Некоторое время спустя я говорю вам: «Хорошо, приготовьтесь назвать мне последовательность чисел».
    Этот шаг соответствует вызову функции генератора, которая возвращает объект генератора. Обратите внимание, что вы пока не называете мне никаких цифр; вы просто берете бумагу и карандаш.
  • Я прошу вас: «Назовите мне следующий номер», а вы назовете мне первое число; после этого вы ждете, пока я спрошу у вас следующий номер. Ваша задача - вспомнить, где вы были, какие числа вы уже сказали и какое будет следующее число. Меня не волнуют детали.
    Этот шаг соответствует вызову .next() для объекта-генератора.
  • … Повторять предыдущий шаг, пока…
  • в конце концов, вам может прийти конец. Вы не называете мне номер; вы просто кричите: «Держите лошадей! Я закончил! Больше никаких номеров!»
    Этот шаг соответствует завершению работы объекта-генератора и возникновению исключения StopIteration. Функция генератора не должна вызывать исключение. Он возникает автоматически, когда функция завершается или выдает return.

Это то, что делает генератор (функция, содержащая yield); он начинает выполнение, приостанавливается всякий раз, когда выполняет yield, а когда его запрашивают значение .next(), он продолжает с того места, где он был последним. Он идеально сочетается по дизайну с протоколом итератора Python, который описывает, как последовательно запрашивать значения.

Самым известным пользователем протокола итератора является команда for в Python. Итак, всякий раз, когда вы делаете:

for item in sequence:

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

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

Для получения более точной информации прочтите о типы итераторов, заявление о доходности и генераторы в документации Python.

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

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);

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

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

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

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

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

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

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

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 - это повторяемый, потому что он реализует протокол итератора. В определяемом пользователем классе вы можете реализовать метод __iter__(), чтобы сделать экземпляры вашего класса итеративными. Этот метод должен возвращать итератор. Итератор - это объект с методом next(). Возможно реализовать как __iter__(), так и next() в одном классе, и __iter__() будет возвращать 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() может выглядеть очень сложным и подверженным ошибкам. Здесь генераторы обеспечивают чистое и простое решение.

«Когда вы видите функцию с операторами yield, примените этот простой трюк, чтобы понять, что произойдет» Разве это полностью не игнорирует тот факт, что вы можете использовать send в генераторе, что является огромной частью функций генераторов?
DanielSank 18.06.2017 01:41

«это может быть цикл for, но это также может быть код вроде otherlist.extend(mylist)» -> Это неверно. extend() изменяет список на месте и не возвращает итерацию. Попытка зациклить otherlist.extend(mylist) не удастся с TypeError, потому что extend() неявно возвращает None, и вы не можете зациклить None.

Pedro 14.09.2017 17:48

@pedro Вы неправильно поняли это предложение. Это означает, что python выполняет два упомянутых шага на mylist (не на otherlist) при выполнении otherlist.extend(mylist).

today 26.12.2017 21:53

Ключевое слово 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]]

Пример

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


Мелочи

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

Говоря языком 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 для копируемого итератора было отложено.

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

Как генератор Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Использование лексических замыканий вместо генераторов

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Использование замыканий объектов вместо генераторов (потому что ClosuresAndObjectsAreEquivalent)

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

Yield дает вам генератор.

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 для проталкивания вещей через стек вызовов вместо использования какой-либо переменной стека. Генераторы также могут использоваться для специализированного обхода дерева и для многих других вещей.

Просто примечание - в Python 3 range также возвращает генератор вместо списка, поэтому вы также увидите аналогичную идею, за исключением того, что __repr__ / __str__ переопределяются, чтобы показать лучший результат, в данном случае range(1, 10, 2).

It'sNotALie. 21.03.2019 21:33

Для тех, кто предпочитает минимальный рабочий пример, подумайте об этом интерактивном сеансе 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

Я собирался написать «прочтите страницу 19 книги Бизли« Python: Essential Reference »для быстрого описания генераторов», но многие другие уже опубликовали хорошие описания.

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

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

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

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

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

Продолжения в этой более общей форме могут быть реализованы двумя способами. В методе call/cc стек программы буквально сохраняется, а затем при вызове продолжения стек восстанавливается.

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

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

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

Остальная часть этого поста будет, без потери общности, концептуализировать продолжения как CPS, потому что это чертовски легче понять и прочитать.


Теперь поговорим о генераторах в Python. Генераторы - это особый подтип продолжения. Тогда как продолжения могут в целом сохранять состояние вычисление (то есть стек вызовов программы), генераторы могут сохранять состояние итерации только через итератор. Хотя это определение немного вводит в заблуждение для некоторых случаев использования генераторов. Например:

def f():
  while True:
    yield 4

Очевидно, что это разумная итерация, поведение которой четко определено - каждый раз, когда генератор повторяет ее, он возвращает 4 (и делает это навсегда). Но, вероятно, при мысли об итераторах приходит на ум не прототип итерируемого объекта (например, for x in collection: do_something(x)). Этот пример иллюстрирует возможности генераторов: если что-то является итератором, генератор может сохранить состояние своей итерации.

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

Но вы можете легко реализовать (и концептуализировать) генераторы как простой, конкретный случай стиля передачи продолжения:

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

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

где ключевое слово yield на самом деле является синтаксическим сахаром для реальной функции генератора, в основном что-то вроде:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

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

Вот мысленный образ того, что делает yield.

Мне нравится думать о потоке как о стеке (даже если он не реализован таким образом).

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

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

Так что это своего рода замороженная функция, на которой висит генератор.

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

Сравните следующие примеры:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

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

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

Вызов yielderFunction() не запускает его код, а создает генератор из кода. (Может быть, было бы неплохо называть такие вещи префиксом yielder для удобства чтения.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

В полях gi_code и gi_frame хранится замороженное состояние. Изучая их с помощью dir(..), мы можем подтвердить, что наша ментальная модель, приведенная выше, заслуживает доверия.

С точки зрения программирования итераторы реализованы как thunks.

Чтобы реализовать итераторы, генераторы и пулы потоков для одновременного выполнения и т. д. В качестве преобразователей, используют сообщения, отправленные объекту закрытия, у которого есть диспетчер, и диспетчер отвечает на "сообщения".

«следующий» - это сообщение, отправленное на закрытие, созданное вызовом «iter».

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

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

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->

Вот простой пример:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Выход:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

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

Вроде бы интересная и приятная способность: D

Ты прав. Но каково влияние на поток, чтобы увидеть поведение «урожайности»? Я могу изменить алгоритм во имя математики. Поможет ли получить иную оценку «урожайности»?

Engin OZTURK 02.07.2018 04:44

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

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

Например:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

Милый! батут (в смысле Лиспа). Нечасто их видишь!

00prometheus 04.12.2015 21:31

Есть еще одно использование и значение yield (начиная с Python 3.3):

yield from <expr>

От PEP 380 - Синтаксис для делегирования субгенератору:

A syntax is proposed for a generator to delegate part of its operations to another generator. This allows a section of code containing 'yield' to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator.

The new syntax also opens up some opportunities for optimisation when one generator re-yields values produced by another.

Более того, это представит (начиная с Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

чтобы избежать путаницы сопрограмм с обычным генератором (сегодня в обоих используется yield).

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

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

def getNextLines():
   while con.isOpen():
       yield con.read()

Вы можете использовать его в своем коде следующим образом:

for line in getNextLines():
    doSomeThing(line)

Попытка передать управление выполнением

При выполнении yield управление выполнением будет передано из getNextLines () в цикл for. Таким образом, каждый раз, когда вызывается getNextLines (), выполнение начинается с того места, где оно было приостановлено в прошлый раз.

Таким образом, вкратце, функция со следующим кодом

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

напечатает

"first time"
"second time"
"third time"
"Now some useful value 12"

Доходность - это объект

return в функции вернет единственное значение.

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

Что еще более важно, yield - это барьер.

like barrier in the CUDA language, it will not transfer control until it gets completed.

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

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

Ключевое слово yield просто собирает возвращаемые результаты. Думайте о yield как о return +=

Вот простой подход, основанный на yield, для вычисления ряда Фибоначчи:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Когда вы вводите это в свой REPL, а затем пытаетесь вызвать его, вы получите загадочный результат:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

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

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

Используя встроенную функцию next(), вы напрямую вызываете .next / __next__, заставляя генератор выдавать значение:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Косвенно, если вы предоставите fib циклу for, инициализатору list, инициализатору tuple или чему-либо еще, что ожидает объект, который генерирует / производит значения, вы «потребляете» генератор до тех пор, пока он не перестанет генерировать значения ( и он возвращается):

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Аналогично с инициализатором tuple:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

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

Когда вы впервые вызываете fib, вызывая его:

f = fib()

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

Когда вы затем запрашиваете, он генерирует первое значение, прямо или косвенно, он выполняет все найденные операторы, пока не встретит yield, затем возвращает значение, которое вы предоставили yield, и приостанавливает работу. В качестве примера, который лучше демонстрирует это, давайте воспользуемся некоторыми вызовами print (замените на print "text", если на Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Теперь введите в REPL:

>>> gen = yielder("Hello, yield!")

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

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Результаты без кавычек - это то, что напечатано. Цитируемый результат - это результат, возвращаемый yield. Позвоните в next еще раз:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Генератор запоминает, что он был приостановлен на yield value, и возобновляет работу оттуда. Следующее сообщение печатается, и снова выполняется поиск оператора yield, чтобы приостановить его (из-за цикла while).

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

Когда yield используется вместо return в функции Python, эта функция превращается в нечто особенное, называемое generator function. Эта функция вернет объект типа generator. Ключевое слово yield - это флаг, уведомляющий компилятор python о необходимости особого обращения с такой функцией. Нормальные функции прекращают свою работу, как только из нее возвращается какое-то значение. Но с помощью компилятора функция генератора можно думать о возобновляется. То есть контекст выполнения будет восстановлен, и выполнение продолжится с последнего запуска. Пока вы явно не вызовете return, что вызовет исключение StopIteration (которое также является частью протокола итератора), или не дойдете до конца функции. Я нашел много ссылок на generator, но этот один из functional programming perspective является наиболее усваиваемым.

(Теперь я хочу поговорить о причинах, лежащих в основе generator и iterator, основываясь на моем собственном понимании. Надеюсь, это поможет вам понять основная мотивация итератора и генератора. Такая концепция проявляется и на других языках, таких как C#.)

Насколько я понимаю, когда мы хотим обработать кучу данных, мы обычно сначала где-то храним данные, а затем обрабатываем их по очереди. Но этот подход наивный проблематичен. Если объем данных огромен, заранее хранить их целиком дорого. Поэтому вместо того, чтобы хранить сам data напрямую, почему бы не сохранить какой-нибудь metadata косвенно, то есть the logic how the data is computed?.

Есть 2 подхода для обертывания таких метаданных.

  1. В объектно-ориентированном подходе мы оборачиваем метаданные as a class. Это так называемый iterator, который реализует протокол итератора (то есть методы __next__() и __iter__()). Это также часто встречающийся шаблон проектирования итератора.
  2. Функциональный подход, мы обертываем метаданными as a function. Это так называемый generator function. Но под капотом возвращенный generator object все еще итератор IS-A, потому что он также реализует протокол итератора.

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

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 - это способ сказать сладкий

build a series of stuff

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

>>> 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), выполнение смещается вперед до следующего yield. 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, потому что оба значения применяется:

yield — produce or provide (as in agriculture)

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

yield — give way or relinquish (as in political power)

... прекратить выполнение ЦП, пока итератор не продвинется вперед.

Еще один TL; DR

Итератор в списке: next() возвращает следующий элемент списка

Генератор итератора: next() вычислит следующий элемент на лету (выполнить код)

Вы можете увидеть yield / generator как способ вручную запустить поток управления извне (например, продолжить цикл на один шаг), вызвав next, каким бы сложным ни был поток.

Примечание: Генератор НЕТ является нормальной функцией. Он запоминает предыдущее состояние как локальные переменные (стек). См. Другие ответы или статьи для подробного объяснения. Генератор может быть только повторяется один раз. Вы могли бы обойтись без yield, но это было бы не так хорошо, поэтому его можно считать «очень красивым» языковым сахаром.

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

Вот пример, для которого определенно лучше всего подходит yield:

return (in function)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

yield (in function)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Calling functions

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

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

This is the result from the code:

Output

Как видите, обе функции делают одно и то же. Единственная разница в том, что return_dates() дает список, а yield_dates() дает генератор.

Пример из реальной жизни - это что-то вроде чтения файла построчно или если вы просто хотите создать генератор.

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

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

просто выводит

one
two
three

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

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

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

и используйте это так;

for i in myRangeNaive(10):
    print i

Но это неэффективно, потому что

  • Вы создаете массив, который используете только один раз (это тратит впустую память)
  • Этот код фактически дважды перебирает этот массив! :(

К счастью, Гвидо и его команда были достаточно щедры, чтобы разработать генераторы, так что мы могли просто сделать это;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Теперь на каждой итерации функция в генераторе с именем next() выполняет функцию до тех пор, пока она либо не достигнет оператора yield, в котором она останавливается и «возвращает» значение, либо не достигает конца функции. В этом случае при первом вызове next() выполняется до оператора yield и yield 'n', при следующем вызове он выполнит оператор приращения, вернется к 'while', оценит его, и если истина, он остановится. и снова дайте 'n', так будет продолжаться до тех пор, пока условие while не вернет false и генератор не перейдет к концу функции.

Простой пример, чтобы понять, что это такое: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

Результат:

1 2 1 2 1 2 1 2

ты уверен в этом выходе? Разве это не будет напечатано только в одной строке, если вы запустили этот оператор печати с помощью print(i, end=' ')? В противном случае я считаю, что поведение по умолчанию поместит каждое число в новую строку

user9074332 05.02.2020 07:05

@ user9074332, Вы правы, но написано в одну строчку для облегчения понимания

Gavriel Cohen 05.02.2020 17:58

урожай похож на return. Разница в следующем:

урожай делает функцию итерируемой (в следующем примере функция primes(n = 1) становится итерируемой). По сути, это означает, что при следующем вызове функции она продолжится с того места, где она была оставлена ​​(то есть после строки yield expression).

def isprime(n):
    if n == 1:
        return False
    for x in range(2, n):
        if n % x == 0:
            return False
    else:
        return True

def primes(n = 1):
   while(True):
       if isprime(n): yield n
       n += 1 

for n in primes():
    if n > 100: break
    print(n)

В приведенном выше примере, если isprime(n) истинно, он вернет простое число. В следующей итерации он продолжится со следующей строки

n += 1  

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

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

Ваш код проходит через двоичную древовидную структуру. Возьмем, к примеру, это дерево:

    5
   / \
  3   6
 / \   \
1   4   8

И еще одна более простая реализация обхода дерева двоичного поиска:

class Node(object):
..
def __iter__(self):
    if self.has_left_child():
        for child in self.left:
            yield child

    yield self.val

    if self.has_right_child():
        for child in self.right:
            yield child

Код выполнения находится на объекте Tree, который реализует __iter__ следующим образом:

def __iter__(self):

    class EmptyIter():
        def next(self):
            raise StopIteration

    if self.root:
        return self.root.__iter__()
    return EmptyIter()

Оператор while candidates можно заменить на for element in tree; Python переведет это на

it = iter(TreeObj)  # returns iter(self.root) which calls self.root.__iter__()
for element in it: 
    .. process element .. 

Поскольку функция Node.__iter__ является генератором, код внутри него выполняется на каждой итерации. Таким образом, казнь будет выглядеть так:

  1. корневой элемент - первый; проверьте, оставил ли он дочерние элементы, и for выполнит их итерацию (назовем его it1, потому что это первый объект итератора)
  2. у него есть дочерний элемент, поэтому выполняется for. for child in self.left создает новый итератор из self.left, который сам является объектом узла (it2).
  3. Та же логика, что и 2, но создается новый iterator (it3)
  4. Теперь мы достигли левого конца дерева. У it3 нет левых дочерних элементов, поэтому он продолжается, а yield self.value
  5. При следующем вызове next(it3) он вызывает StopIteration и существует, поскольку у него нет правильных дочерних элементов (он достигает конца функции, ничего не возвращая)
  6. it1 и it2 все еще активны - они не исчерпаны, и вызов next(it2) даст значения, а не повысит StopIteration
  7. Теперь мы вернулись к контексту it2 и вызываем next(it2), который продолжается с того места, где он остановился: сразу после оператора yield child. Поскольку у него больше нет левых дочерних элементов, он продолжает работу и возвращает его self.val.

Уловка здесь в том, что каждая итерация создает под-итераторы проходит по дереву и сохраняет состояние текущего итератора. Достигнув конца, он возвращается по стеку, и значения возвращаются в правильном порядке (сначала возвращается наименьшее значение).

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

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

Все ответы отличные, но для новичков это немного сложно.

Я предполагаю, что вы выучили инструкцию return.

По аналогии, return и yield - близнецы. return означает «вернуться и остановиться», тогда как «выход» означает «вернуться, но продолжить».

  1. Try to get a num_list with return.
def num_list(n):
    for i in range(n):
        return i

Запустить его:

In [5]: num_list(3)
Out[5]: 0

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

  1. There comes yield

Замените return на yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Теперь вы выигрываете, чтобы получить все числа.

По сравнению с return, который запускается один раз и останавливается, yield проходит запланированное вами время. Вы можете интерпретировать return как return one of them и yield как return all of them. Это называется iterable.

  1. One more step we can rewrite yield statement with return
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

Это суть про yield.

Разница между выводом списка return и выводом объекта yield заключается в следующем:

Вы всегда будете получать [0, 1, 2] из объекта списка, но сможете получить их только один раз из «вывода объекта yield». Итак, у него есть новое имя объекта generator, как показано в Out[11]: <generator object num_list at 0x10327c990>.

В заключение, в качестве метафоры для грока:

  • return и yield близнецы
  • list и generator близнецы

Это понятно, но одно из основных отличий заключается в том, что вы можете иметь несколько значений доходности в функции / методе. На этом аналогия полностью разрушается. Yield запоминает свое место в функции, поэтому в следующий раз, когда вы вызовете next (), ваша функция перейдет к следующему yield. Я думаю, это важно, и это нужно выразить.

Mike S 23.08.2018 16:27

В Python generators (специальный тип iterators) используются для генерации серий значений, а ключевое слово yield аналогично ключевому слову return функций генератора.

Другая интересная вещь, которую делает ключевое слово yield, - это сохранение state функции генератора..

Итак, мы можем устанавливать для number другое значение каждый раз, когда generator уступает.

Вот пример:

def getPrimes(number):
    while True:
        if isPrime(number):
            number = yield number     # a miracle occurs here
        number += 1

def printSuccessivePrimes(iterations, base=10):
    primeGenerator = getPrimes(base)
    primeGenerator.send(None)
    for power in range(iterations):
        print(primeGenerator.send(base ** power))

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

In [4]: def make_cake(numbers):
   ...:     for i in range(numbers):
   ...:         yield 'Cake {}'.format(i)
   ...:

In [5]: factory = make_cake(5)

Здесь factory называется генератором, который делает вам пирожные. Если вы вызовете make_function, вы получите генератор вместо запуска этой функции. Это потому, что когда ключевое слово yield присутствует в функции, она становится генератором.

In [7]: next(factory)
Out[7]: 'Cake 0'

In [8]: next(factory)
Out[8]: 'Cake 1'

In [9]: next(factory)
Out[9]: 'Cake 2'

In [10]: next(factory)
Out[10]: 'Cake 3'

In [11]: next(factory)
Out[11]: 'Cake 4'

Они съели все лепешки, но снова просят один.

In [12]: next(factory)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-12-0f5c45da9774> in <module>
----> 1 next(factory)

StopIteration:

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

In [13]: factory = make_cake(3)

In [14]: for cake in factory:
    ...:     print(cake)
    ...:
Cake 0
Cake 1
Cake 2

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

Еще один пример: допустим, вам нужен случайный пароль, когда вы его запрашиваете.

In [22]: import random

In [23]: import string

In [24]: def random_password_generator():
    ...:     while True:
    ...:         yield ''.join([random.choice(string.ascii_letters) for _ in range(8)])
    ...:

In [25]: rpg = random_password_generator()

In [26]: for i in range(3):
    ...:     print(next(rpg))
    ...:
FXpUBhhH
DdUDHoHn
dvtebEqG

In [27]: next(rpg)
Out[27]: 'mJbYRMNo'

Здесь rpg - генератор, который может генерировать бесконечное количество случайных паролей. Таким образом, мы также можем сказать, что генераторы полезны, когда мы не знаем длины последовательности, в отличие от списка, который имеет конечное число элементов.

Здесь можно понять аналогию:

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

Генераторы Python не сильно отличаются от этой концепции.

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

Код машины:

def barcode_generator():
    serial_number = 10000  # Initial barcode
    while True:
        yield serial_number
        serial_number += 1


barcode = barcode_generator()
while True:
    number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
    barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
    print(barcodes)

    # function_to_create_the_next_batch_of_lightbulbs(barcodes)

    produce_more = input("Produce more? [Y/n]: ")
    if produce_more == "n":
        break

Как видите, у нас есть автономная «функция» для генерации следующего уникального серийного номера каждый раз. Эта функция возвращает генератор! Как видите, мы не вызываем функцию каждый раз, когда нам нужен новый серийный номер, но мы используем next() с генератором для получения следующего серийного номера.

Выход:

How many lightbulbs to generate? 5
[10000, 10001, 10002, 10003, 10004]
Produce more? [Y/n]: y
How many lightbulbs to generate? 6
[10005, 10006, 10007, 10008, 10009, 10010]
Produce more? [Y/n]: y
How many lightbulbs to generate? 7
[10011, 10012, 10013, 10014, 10015, 10016, 10017]
Produce more? [Y/n]: n

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

def fun():
   yield 1
   yield 2
   yield 3

И звонящий: -

def caller():
   print ('First value printing')
   print (fun())
   print ('Second value printing')
   print (fun())
   print ('Third value printing')
   print (fun())

Приведенный выше сегмент кода (функция вызывающего абонента) при вызове выводит: -

First value printing
1
Second value printing
2
Third value printing
3

Как видно из вышеизложенного, yield возвращает значение вызывающей стороне, но при повторном вызове функции она начинается не с первого оператора, а с оператора сразу после yield. В приведенном выше примере была напечатана «Печать первого значения» и была вызвана функция. 1 был возвращен и напечатан. Затем была напечатана «печать второго значения» и снова был вызван fun (). Вместо того, чтобы печатать 1 (первый оператор), он вернул 2, т.е. оператор сразу после yield 1. Тот же процесс повторяется и дальше.

Если вы попытаетесь запустить этот код, print(fun()) не печатает числа. Вместо этого он печатает представление объекта-генератора, возвращаемого fun() (что-то вроде <generator object fun at 0x6fffffe795c8>).

Funny Geeks 30.04.2020 21:03

@FunnyGeeks Я запустил тот же код на Jupyter Notebook, и он отлично работает. Кроме того, здесь нужно было объяснить работу ключевого слова yield. Фрагмент предназначен только для демонстрационных целей.

Swati Srivastava 02.05.2020 23:16

Я пробовал это в python2 и python3 в моей консоли cygwin. Это не сработало. github.com/ImAmARobot/PythonTest

Funny Geeks 04.05.2020 00:31

Также можно отправлять данные обратно в генератор!

Действительно, как объясняют многие ответы здесь, использование yield создает generator.

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

Пример:

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

def translator():
    # load all the words in English language and the translation to 'other lang'
    my_words_dict = {'hello': 'hello in other language', 'dog': 'dog in other language'}

    while True:
        word = (yield)
        yield my_words_dict.get(word, 'Unknown word...')

Бег:

my_words_translator = translator()

next(my_words_translator)
print(my_words_translator.send('dog'))

next(my_words_translator)
print(my_words_translator.send('cat'))

напечатает:

dog in other language
Unknown word...

Чтобы обобщить:

используйте метод send внутри генератора для отправки данных обратно в генератор. Для этого используется (yield).

Другие вопросы по теме