Возврат или выход из функции, которая вызывает генератор?

30

У меня есть генератор, generatorа также удобный метод generate_all.

def generator(some_list):
  for i in some_list:
    yield do_something(i)

def generate_all():
  some_list = get_the_list()
  return generator(some_list) # <-- Is this supposed to be return or yield?

Должен generate_all returnили yield? Я хочу, чтобы пользователи обоих методов использовали его одинаково, т.е.

for x in generate_all()

должно быть равно

some_list = get_the_list()
for x in generate(some_list)
hyankov
источник
2
Есть причина, чтобы использовать либо. В этом примере возвращение более эффективно
Безумный физик
1
Это напоминает мне о похожем вопросе, который я однажды задал: «yield from iterable» против «return iter (iterable)» . Хотя это не относится конкретно к генераторам, это в основном то же самое, что генераторы и итераторы очень похожи в Python. Также здесь может быть полезна стратегия сравнения байт-кода, предложенная ответом.
PeterE

Ответы:

12

Генераторы лениво оценивают так returnили yieldбудут вести себя по-другому, когда вы отлаживаете код или выдается исключение.

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

def generator(some_list):
    for i in some_list:
        raise Exception('exception happened :-)')
        yield i

def generate_all():
    some_list = [1,2,3]
    return generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-3-b19085eab3e1> in <module>
      8     return generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-3-b19085eab3e1> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

И если он использует yield from:

def generate_all():
    some_list = [1,2,3]
    yield from generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-4-be322887df35> in <module>
      8     yield from generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-4-be322887df35> in generate_all()
      6 def generate_all():
      7     some_list = [1,2,3]
----> 8     yield from generator(some_list)
      9 
     10 for item in generate_all():

<ipython-input-4-be322887df35> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

Однако это происходит за счет производительности. Дополнительный уровень генератора имеет некоторые накладные расходы. Так returnчто, как правило, будет немного быстрее, чем yield from ...(или for item in ...: yield item). В большинстве случаев это не имеет большого значения, потому что все, что вы делаете в генераторе, обычно доминирует во время выполнения, так что дополнительный слой не будет заметен.

Тем yieldне менее, имеет ряд дополнительных преимуществ: вы не ограничены одной итерацией, вы также можете легко получить дополнительные элементы:

def generator(some_list):
    for i in some_list:
        yield i

def generate_all():
    some_list = [1,2,3]
    yield 'start'
    yield from generator(some_list)
    yield 'end'

for item in generate_all():
    print(item)
start
1
2
3
end

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

map(do_something, get_the_list())          # map
(do_something(i) for i in get_the_list())  # generator expression

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

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

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

MSeifert
источник
17

Вы, вероятно, ищете делегирование генератора (PEP380)

Для простых итераторов, yield from iterableэто просто сокращенная формаfor item in iterable: yield item

def generator(iterable):
  for i in iterable:
    yield do_something(i)

def generate_all():
  yield from generator(get_the_list())

Это довольно лаконично, а также имеет ряд других преимуществ, таких как возможность связывать произвольные / разные итерации!

ti7
источник
О, ты имеешь в виду наименование list? Это плохой пример, а не настоящий код, вставленный в вопрос, я, вероятно, должен его отредактировать.
Гяньков
Да - не бойся, я весьма виноват в примере кода, который даже не запустится, сначала спроси ..
ti7
2
Во-первых, тоже можно быть однострочником :). yield from map(do_something, iterable)или дажеyield from (do_something(x) for x in iterable)
Безумный физик
2
"Это пример кода до конца!"
7
3
Делегирование необходимо только в том случае, если вы сами делаете что-то, кроме возвращения нового генератора. Если вы просто вернете новый генератор, делегирование не требуется. Так что yield fromэто бессмысленно, если ваша обертка не делает что - то еще generator-y.
ShadowRanger
14

return generator(list)делает то, что вы хотите. Но учтите, что

yield from generator(list)

будет эквивалентно, но с возможностью получить больше значений после того, generatorкак исчерпан. Например:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
источник
5
Я считаю , что есть тонкое различие между yield fromи , returnкогда потребитель генератора throwsисключение внутри него - и с другими операциями , которые находятся под влиянием трассировки стека.
WorldSEnder
9

Следующие два утверждения окажутся функционально эквивалентными в данном конкретном случае:

return generator(list)

а также

yield from generator(list)

Последний примерно такой же, как

for i in generator(list):
    yield i

returnОператор возвращает генератор , который вы ищете. yield fromИли yieldоператор поворачивает всю функцию в то , что возвращает генератор, который проходит через один вы ищете.

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

Безумный физик
источник
4

Вы бы returnэто.

yielding * приведет generate_all()к вычислению самого генератора и вызовуnext этого внешнего генератора вернет внутренний генератор, возвращенный первой функцией, а это не то, что вам нужно.

* Не включая yield from

Carcigenicate
источник