Що робить ключове слово "урожайність"?


10187

Яке використання 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 модуля .

Відповіді:


14638

Щоб зрозуміти, що 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 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працюють петлі .


354
yieldне така магічна ця відповідь. Коли ви зателефонуєте до функції, яка містить yieldоператор де завгодно, ви отримаєте об’єкт генератора, але не запускається код. Потім кожен раз, коли ви виймаєте об'єкт з генератора, Python виконує код у функції, поки не надійде до yieldоператора, а потім призупиняє і доставляє об'єкт. Коли ви виймаєте інший об'єкт, Python поновлюється відразу після yieldта продовжує, поки не досягне іншого yield(часто того самого, але пізніше ітерації). Це продовжується до тих пір, поки функція не закінчиться, і в цей момент генератор визнається вичерпаним.
Маттіас Фріпп

29
"Ці ітерабелі зручні ... але ви зберігаєте всі значення в пам'яті, і це не завжди те, що ви хочете", є неправильним або заплутаним. Ітераційний повертає ітератор після виклику iter () на ітерабелі, і ітератор не завжди повинен зберігати його значення в пам'яті, залежно від реалізації методу iter , він також може генерувати значення в послідовності за запитом.
пікет 涅

Було б непогано додати до цієї чудової відповіді, чому це точно те саме, що ви використовували() замість того[] , що саме ()є (може бути плутанина з кортежем).
WoJ

Я можу помилятися, але генератор не є ітератором, "іменований генератор" - ітератор.
адерхокс

2006

Ярлик до розуміння yield

Коли ви бачите функцію з yieldоператорами, застосуйте цей простий фокус, щоб зрозуміти, що буде:

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

Цей трюк може дати вам уявлення про логіку функції, але те, що відбувається насправді yield, суттєво відрізняється від того, що відбувається у списку. У багатьох випадках підхід до продуктивності буде набагато ефективнішим і пам’яті швидшим. В інших випадках цей трюк змусить вас застрягнути у нескінченному циклі, навіть якщо оригінальна функція працює просто чудово. Читайте далі, щоб дізнатися більше ...

Не плутайте своїх Інтерабелів, Ітераторів та Генераторів

По-перше, протокол ітератора - коли ви пишете

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

Python виконує наступні два етапи:

  1. Отримує ітератор для mylist:

    Дзвінок iter(mylist) -> це повертає об'єкт next()методом (або __next__()в Python 3).

    [Це крок, про який люди забувають розповісти]

  2. Використовує ітератор для переходу на елементи:

    Продовжуйте виклик next()методу на ітераторі, поверненому з кроку 1. Значення повернення з next()присвоюється xі виконується тіло циклу. Якщо виняток StopIterationпіднімається зсередини next(), це означає, що в ітераторі більше немає значень, і цикл закінчується.

Правда, Python виконує вищевказані два кроки в будь-який час, коли хоче перевести вміст об'єкта - так що це може бути цикл, але він також може бути кодовим 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()викликається, він не повертає жодного зі значень у виписках про урожай! Він повертає об'єкт генератора. Також функція насправді не виходить - вона переходить у призупинений стан. Коли forцикл намагається перетворити цикл на об'єкт-генератор, функція поновлюється зі свого призупиненого стану в наступному рядку після того, як yieldвін раніше повернувся з, виконує наступний рядок коду, в цьому випадку, yieldоператор і повертає його як наступний пункт. Це відбувається до тих пір, поки функція не завершиться, і в цей момент генератор піднімається StopIteration, і цикл закінчується.

Таким чином, об’єкт генератора на зразок адаптера - в одному кінці він демонструє протокол ітератора, викриваючи __iter__()та next()методи підтримувати forцикл щасливим. На іншому кінці, однак, він запускає функцію достатньо, щоб отримати наступне значення з неї і повертає її назад у призупинений режим.

Навіщо використовувати генератори?

Зазвичай ви можете написати код, який не використовує генераторів, але реалізує ту саму логіку. Одним із варіантів є використання тимчасового «трюку», про який я згадував раніше. Це не спрацює у всіх випадках, наприклад, якщо у вас є нескінченна кількість циклів, або це може неефективно використовувати пам'ять, якщо у вас справді довгий список. Інший підхід полягає у впровадженні нового ітерабельного класу SomethingIter, який утримує стан членів екземпляра та виконує наступний логічний крок у його next()(або __next__()в Python 3) методі. Залежно від логіки, код всередині next()методу може виглядати дуже складним і схильним до помилок. Тут генератори забезпечують чисте і просте рішення.


20
"Коли ви бачите функцію із заявами про врожайність, застосуйте цей простий фокус, щоб зрозуміти, що буде" Чи не повністю це ігнорується той факт, що ви можете sendперетворитись на генератор, який є величезною частиною точки генераторів?
DanielSank

10
"це може бути цикл for, але це також може бути код на зразок otherlist.extend(mylist)" -> Це неправильно. extend()змінює список на місці і не повертає ітерабельний номер. Спроба переосмислитись otherlist.extend(mylist)не вдасться через те, TypeErrorщо extend()неявно повертається None, і ви не можете переосмислити цикл None.
Педро

4
@pedro Ви неправильно зрозуміли це речення. Це означає, що пітон виконує два згадані кроки на mylist(не увімкнено otherlist) при виконанні otherlist.extend(mylist).
сьогодні

555

Подумайте про це так:

Ітератор - це просто фантазійний звуковий термін для об'єкта, який має next()метод. Таким чином, функція з урожаєм закінчується таким чином:

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

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

for i in some_function():
    print i

Це в основному те, що робить інтерпретатор Python із наведеним вище кодом:

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

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

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

def some_func():
    return it()

for i in some_func():
    print i

Для отримання більш детальної інформації про те, що відбувається за лаштунками, forцикл можна переписати на це:

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

Це має більше сенсу чи просто вас більше бентежить? :)

Я хотів би відзначити , що це є спрощенням в ілюстративних цілях. :)


1
__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

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

Де forреалізація циклу, яку ви написали, називає __iter__метод iterator, екземплярний екземпляр it?
Систематична дисинтеграція

455

yieldКлючові слова зводяться до двох простих фактів:

  1. Якщо компілятор виявляє yieldключове слово де-небудь всередині функції, ця функція більше не повертається через returnоператор. Натомість він одразу повертає ледачий об'єкт "відкладений список", який називається генератором
  2. Генератор є ітерабельним. Що таке ітерабельність ? Це що - то , як listабо setабо rangeабо Dict ракурс, з вбудованим протоколом для відвідування кожного елемента в певному порядку .

Коротше кажучи: генератор - це ледачий, поступово відкладений список , і 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яка подібна до Python range. Виклик makeRange(n)ПОВЕРНЕННЯ ГЕНЕРАТОРУ:

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

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

Щоб змусити генератор негайно повернути свої очікувані значення, ви можете передати його list()(як і будь-який ітерабельний):

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

Приклад порівняння з "просто поверненням списку"

Наведений вище приклад можна вважати просто створенням списку, який ви додаєте та повертаєте:

# 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], а ітератор - це конкретний екземпляр запитуваного типу-циклу [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 стандартів була відкладена.


377

Що робить yieldключове слово в Python?

Конспект / резюме відповіді

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

Генератори:

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

Ідея для генераторів походить з інших мов (див. Виноску 1) із різними реалізаціями. У генераторах Python - це виконання коду заморожено в точці виходу. Після виклику генератора (методи обговорюються нижче) виконання поновлюється і потім заморожується при наступному виході.

yieldнадає простий спосіб реалізації протоколу ітератора , визначеного наступними двома методами: __iter__і next(Python 2) або __next__(Python 3). Обидва ці методи роблять об'єкт ітератором, який ви можете перевірити за Iteratorдопомогою 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те саме, що дзвонити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робить багато. Я впевнений, що міг би додати ще ґрунтовніші приклади до цього. Якщо ви хочете більше або маєте якусь конструктивну критику, повідомте мене, коментуючи нижче.


Додаток:

Критика верхнього / прийнятого відповіді **

  • Він плутається в тому, що робить ітерабельним , просто використовуючи список як приклад. Дивіться мої посилання вище, але підсумовуючи: ітерабельний __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

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

Розробники основних процесорів CPython обговорюють знецінення його надбавки . Ось відповідна публікація зі списку розсилки:

30 січня 2017 року о 19:05 Бретт Кеннон написав:

У Нд, 29 січня 2017 о 16:39 Крейг Родрігес написав:

Я в порядку з будь-яким підходом. Залишати речі такими, як вони є в Python 3, це не добре, ІМХО.

Моє голосування - це синтаксис, оскільки ви не отримуєте того, чого очікуєте від синтаксису.

Я погоджуюся, що для нас це є розумним місцем, оскільки будь-який код, що покладається на поточну поведінку, дійсно занадто розумний, щоб бути доцільним.

Що стосується того, щоб потрапити туди, ми, швидше за все, захочемо:

  • Синтаксичне попередження або депрекаціяПопередження в 3.7
  • Попередження Py3k у 2.7.x
  • SyntaxError в 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 Threading , який навіть в деяких системах не доступний.

  2. Це означає, наприклад, що xrangeоб'єкти ( rangeв Python 3) не є Iterator, хоча вони ітерабельні, тому що їх можна повторно використовувати. Як і списки, їхні __iter__методи повертають об'єкти ітератора.

  3. yieldспочатку було введено як оператор, що означає, що воно може з'являтися лише на початку рядка в блоці коду. Тепер yieldстворюється вираз урожайності. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Ця зміна була запропонована, щоб дозволити користувачеві надсилати дані в генератор так само, як може їх отримати. Щоб надсилати дані, ви повинні мати можливість їх привласнити до чогось, і для цього оператор просто не працюватиме.


328

yieldтак само return- він повертає все, що ви їм скажете (як генератор). Різниця полягає в тому, що наступного разу, коли ви викликаєте генератор, виконання починається від останнього виклику до yieldоператора. На відміну від віддачі, кадр стека не очищається, коли виникає вихід, однак управління передається назад абоненту, тому його стан відновиться наступного разу, коли функція буде викликана.

Що стосується вашого коду, функція get_child_candidatesдіє як ітератор, так що, коли ви розширюєте свій список, він додає по черзі один елемент до нового списку.

list.extendвикликає ітератор до вичерпання. У випадку опублікованого вами зразка коду було б набагато зрозуміліше просто повернути кортеж і додати його до списку.


107
Це близько, але не правильно. Кожен раз, коли ви викликаєте функцію із заявою про вихід, вона повертає абсолютно новий об'єкт генератора. Лише тоді, коли ви викликаєте метод .next () цього генератора, виконання виконується після останнього результату.
kurosch

239

Зазначимо ще одну додаткову річ: функція, яка в дійсності насправді не повинна припинятися. Я написав такий код:

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

Це дійсно допомагає спростити деякі проблеми та полегшує роботу з деякими речами.


233

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

209

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оскільки застосовуються обидва значення :

урожай - виробляти або забезпечувати (як у сільському господарстві)

... надайте наступні дані в серії.

поступатися - поступатися чи відмовлятися (як у політичній владі)

... відмовитися від виконання процесора, поки ітератор не просунеться.


194

Вихід дає генератор.

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 мільярдів. :)

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


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

189

Це повернення генератора. Я не особливо знайомий з Python, але я вважаю, що це те саме, що ітератор блоки C #, якщо ви знайомі з ними.

Ключова ідея полягає в тому, що компілятор / інтерпретатор / що завгодно хитрує, що стосується абонента, він може продовжувати дзвінки next (), і він буде тримати повернені значення - як якщоби метод генератора був призупинений . Тепер, очевидно, ви не можете реально "призупинити" метод, тому компілятор створює машину стану, щоб ви пам'ятали, де ви зараз перебуваєте і як виглядають локальні змінні тощо. Це набагато простіше, ніж самостійно писати ітератор.


167

Є один тип відповідей, який, як мені здається, ще не дано, серед безлічі чудових відповідей, які описують, як користуватися генераторами. Ось відповідь теорії мови програмування:

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)

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

Решта цього посту, не втрачаючи загальності, концептуалізує продовження як CPS, оскільки це набагато простіше зрозуміти та прочитати.


Тепер поговоримо про генератори в Python. Генератори є специфічним підтипом продовження. Оскільки продовження в цілому може зберегти стан обчислень (тобто стек викликів програми), генератори можуть лише зберегти стан ітерації над ітератором . Хоча це визначення дещо вводить в оману в певних випадках використання генераторів. Наприклад:

def f():
  while True:
    yield 4

Це, очевидно, розумний ітерабел, поведінка якого чітко визначено - кожен раз, коли генератор повторюється над ним, він повертає 4 (і робить це назавжди). Але це, мабуть, не прототипний тип ітерабельного, який приходить в голову при думці про ітераторів (тобто for x in collection: do_something(x)). Цей приклад ілюструє потужність генераторів: якщо щось є ітератором, генератор може зберегти стан своєї ітерації.

Повторюю: Продовження можуть зберегти стан стеку програми, а генератори можуть зберегти стан ітерації. Це означає, що продовження набагато потужніше, ніж генератори, але також, що генератори набагато, набагато простіше. Мовному дизайнеру їх легше реалізувати, а програмісту їх легше використовувати (якщо у вас є час для запису, спробуйте прочитати та зрозуміти цю сторінку про продовження та виклик / куб.см ).

Але ви можете легко реалізувати (і концептуалізувати) генератори як простий, специфічний випадок стилю проходження продовження:

Щоразу, коли 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ключового слова.


152

Ось приклад простою мовою. Я надам відповідність між людськими концепціями високого рівня та поняттями Python низького рівня.

Я хочу оперувати послідовністю чисел, але я не хочу турбувати себе створенням цієї послідовності, я хочу зосередитись лише на операції, яку я хочу зробити. Отже, я роблю наступне:

  • Я зателефоную вам і скажу вам, що я хочу послідовність чисел, яка виробляється певним чином, і я даю вам знати, що таке алгоритм.
    Цей крок відповідає defфункціонуванню генератора, тобто функції, що містить a yield.
  • Десь пізніше я кажу тобі: «Добре, готуйся сказати мені послідовність чисел».
    Цей крок відповідає виклику функції генератора, яка повертає об'єкт генератора. Зауважте, що ви ще не кажете мені жодних цифр; ти просто захопиш папір і олівець.
  • Я прошу вас: «скажіть мені наступний номер», а ви мені скажіть перший номер; після цього ви чекаєте, коли я попрошу вас на наступний номер. Ваша робота пам’ятати, де ви були, які цифри ви вже сказали і яке наступне число. Мені не цікаві деталі.
    Цей крок відповідає виклику .next()об’єкта генератора.
  • … Повторити попередній крок, поки…
  • врешті-решт, ви можете закінчитися. Ви не кажете мені номер; ти просто кричиш: "Тримай своїх коней! Я закінчив! Більше немає номерів!"
    Цей крок відповідає об'єкту генератора, який закінчує свою роботу, і підвищує StopIterationвиняток Функція генератора не потребує підвищення винятку. Він піднімається автоматично, коли функція закінчується або видається a return.

Це те, що робить генератор (функція, яка містить a yield); він починає виконувати, паузує щоразу, коли він робить a yield, а коли його запитують, .next()воно продовжує з моменту останнього. Він ідеально вписується в дизайн з протоколом ітератора Python, який описує, як послідовно запитувати значення.

Найвідомішим користувачем протоколу ітератора є forкоманда в Python. Отже, щоразу, коли ви робите:

for item in sequence:

це не має значення , якщо sequenceце список, рядок, словник чи генератор об'єкта , як описано вище; результат той самий: ви читаєте елементи з послідовності один за одним.

Зауважте, що defвведення функції, що містить yieldключове слово, не є єдиним способом створення генератора; це просто найпростіший спосіб створити його.

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


130

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

Щоб зрозуміти, що означає yieldдія у наведеному нижче коді, ви можете за допомогою пальця відстежувати цикл за будь-яким кодом, що містить yield. Кожен раз, коли ваш палець вдаряється yield, вам доведеться чекати введення а nextчи a send. Коли nextвиклик a , ви простежуєте код, поки ви не натиснете на 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()

Симпатичний! Батут (в тому сенсі , лісповскіе). Не часто хтось їх бачить!
00прометей

129

Є ще одне yieldвикористання та значення (починаючи з Python 3.3):

yield from <expr>

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

Синтаксис пропонується генератору делегувати частину своїх операцій іншому генератору. Це дозволяє розрізати розділ коду, що містить "вихід", і розмістити в іншому генераторі. Крім того, субгенератору дозволено повертатися зі значенням, а значення стає доступним генератору делегування.

Новий синтаксис також відкриває деякі можливості для оптимізації, коли один генератор знову отримує значення, вироблені іншим.

Більше того, це введе (з Python 3.5):

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

щоб уникнути плутанини взаємозв'язку зі звичайним генератором (сьогодні yieldвикористовується в обох).


117

Всі чудові відповіді, хоч і трохи непрості для новачків.

Я припускаю, що ви дізналися returnзаяву.

Як аналогія returnі yieldє близнюки. returnозначає "повернення і зупинка", тоді як "вихід" означає "повернення, але продовжуй"

  1. Спробуйте отримати num_list за допомогою return.
def num_list(n):
    for i in range(n):
        return i

Виконати:

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

Дивіться, ви отримуєте лише одне число, а не їх список. returnніколи не дозволяє вам переважати щасливо, просто реалізує один раз і кинеться.

  1. Приходить 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. Ще один крок, з яким ми можемо переписати yieldзаяву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є близнюками

Це зрозуміло, але одна основна відмінність полягає в тому, що у функції / методу ви можете мати кілька виходів. Аналогія повністю розпадається на той момент. Вихід запам'ятовує своє місце у функції, тому наступного разу, коли ви телефонуєте next (), ваша функція продовжується до наступної yield. Я думаю, що це важливо, і це слід виразити.
Майк S

104

Ось кілька прикладів 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)

97

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

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

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


97

З точки зору програмування, ітератори реалізовані як громовідвід .

Щоб реалізувати ітератори, генератори та пули потоків для одночасного виконання тощо, в якості громовідводів (їх також називають анонімними функціями), використовуються повідомлення, що надсилаються об'єкту закриття, який має диспетчер, а диспетчер відповідає на "повідомлення".

http://en.wikipedia.org/wiki/Message_passing

" next " - це повідомлення, надіслане до закриття, створене викликом " iter ".

Існує маса способів здійснення цього обчислення. Я використовував мутацію, але це легко зробити без мутації, повернувши поточне значення та наступний показник.

Ось демонстрація, яка використовує структуру R6RS, але семантика абсолютно ідентична Python. Це та сама модель обчислення, і для її перезапису в 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
->

84

Ось простий приклад:

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утримує позицію програмного потоку і наступний цикл починається з позиції "дохідність". Здається, він чекає на цій посаді, а безпосередньо перед цим повертає значення назовні, і наступного разу продовжує працювати.

Здається, це цікава і приємна здатність: D


Ви праві. Але який вплив на потік має бачити поведінка "врожайності"? Я можу змінити алгоритм в назві математики. Чи допоможе це отримати різні оцінки "врожайності"?
Енґін ОЗТУРК

68

Ось ментальний образ того, що 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(..), ми можемо підтвердити, що наша розумова модель вище є достовірною.


59

Як підказує кожна відповідь, yieldвикористовується для створення генератора послідовностей. Він використовується для динамічної генерації деякої послідовності. Наприклад, читаючи файл у рядку за рядком у мережі, ви можете використовувати yieldфункцію так:

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

Ви можете використовувати його у своєму коді наступним чином:

for line in getNextLines():
    doSomeThing(line)

Виконання контролю передачі gotcha

Елемент керування виконанням буде передано з 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"

59

Простий приклад зрозуміти, що це: 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

5
Ви впевнені в цьому виході? хіба це не буде надруковано лише на одному рядку, якщо ви запустили цей оператор друку за допомогою print(i, end=' ')? В іншому випадку я вважаю, що поведінка за замовчуванням поставить кожне число на новий рядок
user9074332

@ user9074332, Ти маєш рацію, але це написано в одному рядку, щоб полегшити розуміння
Гавриель Коен

57

(Моя відповідь нижче говорить лише з точки зору використання генератора Python, а не від основної реалізації механізму генератора , що передбачає деякі хитрощі маніпуляції зі стеком та купою.)

Якщо yieldвикористовується замість returnфункції python, ця функція перетворюється на щось особливе, що називається generator function. Ця функція поверне об'єкт generatorтипу. Ключове слово прапор , щоб повідомити пітон компілятор для лікування такої функції спеціально. Нормальні функції припиняються, коли якесь значення повертається з нього. Але за допомогою компілятора функцію генератора можна вважати відновлюваною. Тобто, контекст виконання буде відновлений, а виконання триватиме з останнього запуску. Поки ви явно не зателефонуєте return, що призведе до винятку (який також є частиною протоколу ітератора) або не досягне кінця функції. Я знайшов багато посилань про, але цеyieldStopIterationgenerator одногоз самого 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ітератор, оскільки він також реалізує ітераторський протокол.

У будь-якому випадку створюється ітератор, тобто якийсь об’єкт, який може надати вам потрібні дані. Підхід ОО може бути трохи складним. У будь-якому випадку, яким ви будете користуватися, залежить від вас.


54

Підсумовуючи це, yieldоператор перетворює вашу функцію у фабрику, яка виробляє спеціальний об'єкт, який називається a, 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()виконує функцію, поки вона не досягне оператора "урожайності", в якому вона зупиняється і не отримує значення або не доходить до кінця функції. У цьому випадку під час першого виклику next()виконується до оператора дохідності та урожайності 'n', при наступному виклику він виконає оператор приросту, перескочить назад на 'while', оцінить його, і якщо це правда, він зупиниться і знову дамо 'n', воно продовжуватиметься так, доки стан не повернеться хибним, і генератор перейде до кінця функції.


53

Вихід - об’єкт

A returnу функції поверне єдине значення.

Якщо ви хочете, щоб функція повертала величезний набір значень , використовуйте yield.

Що ще важливіше yield- це бар'єр .

як бар'єр у мові CUDA, він не передаватиме контроль, поки не завершиться.

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

Тоді кожен інший виклик ще раз запустить цикл, який ви записали у функції, повертаючи наступне значення, поки не з’явиться жодне значення для повернення.


52

Багато людей використовують, returnа не yield, але в деяких випадкахyield можуть бути ефективнішими та легшими в роботі.

Ось приклад, який yield, безумовно, найкращий для:

повернення (у функції)

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

врожайність (у функції)

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.

Функції виклику

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використовує три лінії замість п’яти і має одну менш змінну для переживання.

Це результат від коду:

Вихідні дані

Як ви бачите, обидві функції роблять те саме. Єдина відмінність - return_dates()це перелік таyield_dates() дає генератор.

Приклад із реального життя може бути чимось на зразок читання файлів рядок за рядком або якщо ви просто хочете зробити генератор.


43

yield- це як елемент повернення для функції. Різниця полягає в тому, що yieldелемент перетворює функцію в генератор. Генератор поводиться так само, як і функція, поки щось не поступається. Генератор зупиняється до наступного виклику і продовжується з тієї ж точки, що і почався. Ви можете отримати послідовність усіх 'вихідних' значень в одному, зателефонувавши list(generator()).



36

Ось простий 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ключовим словом і просто повертає об’єкт генератора у вас. Не дуже корисно це здається.

Коли ви запитуєте, він генерує перше значення, прямо або опосередковано, він виконує всі виявлені ним твердження, поки не зіткнеться з a yield, потім повертає значення, яке ви надали, yieldі призупиняється. Для прикладу, який краще демонструє це, давайте скористаємося деякими printдзвінками (замінимо print "text"if на 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.")

Тепер введіть у відповідь:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.