что - итератор python




Что делает ключевое слово «yield»? (20)

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

Ответить

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

Генераторы:

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

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

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

>>> 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 очень много. Я уверен, что могу добавить к этому еще более подробные примеры. Если вы хотите больше или иметь конструктивную критику, сообщите мне, комментируя ниже.

Приложение:

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

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

Критика ответа, предполагающая yield в выражении или понимании генератора.

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

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

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

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

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

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

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

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

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

Что касается получения там, мы, скорее всего, захотим:

  • Синтаксис Предупреждение или отказ
  • Предупреждение 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 , которые даже не доступны в некоторых системах.

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

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

Каково использование ключевого слова 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 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() может оказаться очень сложным и подверженным ошибкам. Здесь генераторы обеспечивают простое и чистое решение.


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

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

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

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

for i in some_function():
    print i

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

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

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

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

def some_func():
    return it()

for i in some_func():
    print i

Для более глубокого понимания того, что происходит за кулисами, цикл for можно переписать следующим образом:

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

Означает ли это больше смысла или просто путает вас больше? :)

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


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

Генератор считается пустым после запуска функции, но больше не ударяет. Это может быть связано с тем, что цикл закончился, или потому, что вы больше не удовлетворяете "if/else" .

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

Генератор:

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

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

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

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

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

Абонент:

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

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

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

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

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

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

return result

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

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

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

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

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

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

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

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

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

Управление истощением генератора

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

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

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

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

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

Затем просто import itertools .

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

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

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

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

Об этом в этой статье больше говорится о том, как работают петли .


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

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

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


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потому что оба значения применяются:

урожайность - производство или предоставление (как в сельском хозяйстве)

... предоставить следующие данные в серии.

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

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


Вот некоторые примеры 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)

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

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


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

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 миллионов? Мало того, что это огромный eater памяти, он также требует много времени, чтобы построить в то время, когда функция вызывается. Во втором случае бар просто дает вам генератор. Генератор является итерируемым, что означает, что вы можете использовать его в цикле for и т. Д., Но каждое значение может быть доступно только один раз. Все значения также не сохраняются в памяти одновременно; объект-генератор «запоминает», где он был в цикле в последний раз, когда вы его назвали, - таким образом, если вы используете итерируемый (скажем) счет до 50 миллиардов, вам не нужно считать до 50 миллиардов всех сразу и хранить 50 миллиардов номеров для подсчета. Опять же, это довольно надуманный пример,вы, вероятно, будете использовать itertools, если вы действительно хотите считать до 50 миллиардов. :)

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


Еще один TL; DR

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

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

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

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


(Мой ниже ответ говорит только с точки зрения использования генератора 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 +=


Вот мысленный образ того, что 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(..), мы можем подтвердить, что наша ментальная модель выше заслуживает доверия.


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

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

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

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

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

for item in sequence:

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

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

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


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

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

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

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


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

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

return (в функции)

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()дает генератор.

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


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

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


Существует другое yieldиспользование и смысл (с Python 3.3):

yield from <expr>

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

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

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

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

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

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





coroutine