python итератор генераторы - Что делает ключевое слово «yield»?




15 Answers

Ярлык для yield Grokking

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

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

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

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

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

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

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

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

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

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

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

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

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

Здесь mylist является итерабельным, поскольку он реализует протокол итератора. В пользовательском классе вы можете реализовать метод __iter__() чтобы сделать экземпляры вашего класса итерабельными. Этот метод должен возвращать итератор . Итератором является объект со next() методом 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() , он не возвращает никаких значений в f123() yield! Он возвращает объект-генератор. Кроме того, функция действительно не выходит - она ​​переходит в приостановленное состояние. Когда цикл for пытается перебрать объект-генератор, функция возвращается из своего приостановленного состояния в самой следующей строке после возвращаемого ранее результата, выполняет следующую строку кода, в данном случае инструкцию yield и возвращает это как следующий пункт. Это происходит до тех пор, пока функция не выйдет, и в этот момент генератор вызывает StopIteration , и цикл выходит.

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

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

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

инструкция send php

Каково использование ключевого слова 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 метод _get_child_candidates ? Вернулся ли список? Один элемент? Он снова называется? Когда последующие вызовы прекратятся?

1. Код исходит от Jochen Schulz (jrschulz), который создал отличную библиотеку Python для метрических пространств. Это ссылка на полный источник: Module mspace .




Ключевое слово 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- talk , итерабельным является любой объект, который «понимает концепцию for-loop», как список [1,2,3] , а итератор - это конкретный экземпляр запрошенного цикла for, например [1,2,3].__iter__() . Генератор точно такой же, как и любой итератор, за исключением того, как он был написан (с синтаксисом функций).

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

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

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

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




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

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

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




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



TL; DR

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

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

сделай это:

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

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

Это был мой первый «ага» момент с урожаем.

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

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

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

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

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

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

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

Доходность универсальна . Он может выполнять бесконечные циклы:

>>> 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(squares_the_yield_way(4))
[0, 1, 4, 9]

Великолепный выбор слова, 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). Е. ). Этот пример иллюстрирует мощность генераторов: если что-то итератор, генератор может сохранить состояние своей итерации.

Повторить: Continuations может сохранять состояние стека программы, а генераторы могут сохранять состояние итерации. Это означает, что продолжения более мощные, чем генераторы, но также и то, что генераторов много, намного проще. Они легче реализовать разработчику языка, и им проще программировать (если у вас есть время для записи, попробуйте прочитать и понять эту страницу о продолжениях и вызовах / 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ключевого слова.




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

Чтобы понять, что yieldделает в следующем коде, вы можете использовать свой палец для отслеживания цикла через любой код, который имеет yield. Каждый раз, когда ваш палец попадает на него yield, вы должны ждать ввода nextили sendввода. Когда nextвызывается, вы прослеживаете код до тех пор, пока не нажмете yield... код справа от yieldнего оценивается и возвращается вызывающему абоненту ... тогда вы ждете. Когда nextвызывается снова, вы выполняете другой цикл через код. Тем не менее, вы заметите, что в сопрограмме yieldтакже можно использовать с send..., который отправит значение от вызывающего в функцию yielding. Если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()



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

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

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




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

Для реализации итераторов, генераторов и пулов потоков для параллельного выполнения и т. Д. В качестве thunks (также называемых анонимными функциями) используются сообщения, отправленные объекту замыкания с диспетчером, и диспетчер отвечает на «сообщения».

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

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

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

Вот демонстрация, которая использует структуру 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
->



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

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




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

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

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

for line in getNextLines():
    doSomeThing(line)

Контроль выполнения

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



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




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

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

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

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

Существует два подхода к обертке таких метаданных.

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

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




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




Еще один TL; DR

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

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

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

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




Related