Повернення або вихід з функції, яка викликає генератор?


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)

2
Для цього є причина. Для цього прикладу повернення є більш ефективним
Божевільний фізик

1
Це нагадує мені таке запитання я одного разу поставленим: «вихід з Iterable» проти «повернення ITER (ітерація)» . Хоча конкретно не про генераторів, це в основному те саме, що генератори, а ітератори дуже схожі в 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модулі. У таких простих випадках я б просто вдався до цих і лише для нетривіальних випадків напишіть власні генератори.

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


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

Це досить стисло, а також має ряд інших переваг, таких як можливість ланцюга довільних / різних ітерабелів!


О, ви маєте на увазі називання list? Це поганий приклад, не реальний код, вставлений у запитання, я, мабуть, мушу його відредагувати.
hyankov

Так - ніколи не бійся, я цілком винен у прикладі коду, який навіть не буде працювати при першому
питанні

2
По-перше, це може бути і однолінійний :). yield from map(do_something, iterable)або навітьyield from (do_something(x) for x in iterable)
Божевільний фізик

2
"Це приклад коду аж донизу!"
ti7

3
Вам потрібна делегація лише тоді, коли ви самі робите щось інше, ніж просто повернути новий генератор. Якщо ви просто повернете новий генератор, делегація не потрібна. Так що yield fromбезглуздо, якщо ваша обгортка не робить щось інше генератор-у.
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"

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

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