Zwrot lub zysk z funkcji wywołującej generator?

30

Mam generator, generatora także wygodną metodę - 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?

Czy generate_all returnlub yield? Chcę, aby użytkownicy obu metod używali tego samego, tj

for x in generate_all()

powinno być równe

some_list = get_the_list()
for x in generate(some_list)
Hyankov
źródło
2
Jest powód, aby użyć jednego z nich. W tym przykładzie zwrot jest bardziej wydajny
Szalony fizyk
1
Przypomina mi to podobne pytanie, które kiedyś zadałem: „dochód z iteracji” vs „powrót iter (iterowalny)” . Chociaż nie dotyczy generatorów, jest w zasadzie taki sam jak generatory, a iteratory są dość podobne w Pythonie. Przydaje się tutaj także strategia porównywania kodu bajtowego zaproponowana w odpowiedzi.
PeterE

Odpowiedzi:

12

Generatory tak leniwie oceniają returnlub yieldzachowują się inaczej podczas debugowania kodu lub zgłaszania wyjątku.

Z returnkażdym wyjątkiem, który zdarzy się w twoim generatorprzypadku, nie będziesz o niczym wiedział generate_all, to dlatego, że kiedy generatornaprawdę zostanie wykonany, opuściłeś już tę generate_allfunkcję. Z yieldtam będzie miał generate_allślad.

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

A jeśli używa 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 :-)

Jest to jednak kosztem wydajności. Dodatkowa warstwa generatora ma pewne narzuty. returnBędzie więc ogólnie nieco szybszy niż yield from ...(lubfor item in ...: yield item ). W większości przypadków nie będzie to miało większego znaczenia, ponieważ cokolwiek zrobisz w generatorze, zazwyczaj dominuje w czasie wykonywania, więc dodatkowa warstwa nie będzie zauważalna.

yieldMa jednak kilka dodatkowych zalet: nie jesteś ograniczony do jednej iteracji, możesz również z łatwością uzyskać dodatkowe przedmioty:

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

W twoim przypadku operacje są dość proste i nie wiem, czy konieczne jest utworzenie wielu funkcji do tego celu, można łatwo użyć wbudowanego maplub wyrażenia generatora:

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

Oba powinny być identyczne (z wyjątkiem pewnych różnic, gdy zdarzają się wyjątki) do użycia. A jeśli potrzebują bardziej opisowej nazwy, nadal możesz zawinąć je w jedną funkcję.

Istnieje wiele pomocników, które zawierają bardzo popularne operacje na wbudowanych iteracjach, a dalsze można znaleźć we wbudowanym itertoolsmodule. W takich prostych przypadkach po prostu uciekam się do nich i tylko w przypadku spraw innych niż trywialne napisz własne generatory.

Ale zakładam, że twój prawdziwy kod jest bardziej skomplikowany, więc może nie mieć zastosowania, ale pomyślałem, że nie byłby to kompletna odpowiedź bez podania alternatyw.

MSeifert
źródło
17

Prawdopodobnie szukasz Delegacji Generatora (PEP380)

Dla prostych iteratorów yield from iterablejest to po prostu skrócona formafor 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())

Jest dość zwięzły i ma wiele innych zalet, takich jak możliwość łączenia dowolnych / różnych iteracji!

ti7
źródło
Masz na myśli nazywanie list? To zły przykład, nie wklejony w pytaniu prawdziwy kod, prawdopodobnie powinienem go edytować.
hyankov
Tak - nigdy się nie bój, jestem całkiem winny przykładowego kodu, który nawet nie uruchamia się na początku, pytając ..
ti7
2
Pierwszą może być też jedna linijka :). yield from map(do_something, iterable)a nawetyield from (do_something(x) for x in iterable)
Szalony fizyk
2
„To przykładowy kod do samego końca!”
ti7
3
Delegacji potrzebujesz tylko wtedy, gdy sam robisz coś innego niż tylko zwrócenie nowego generatora. Jeśli zwrócisz tylko nowy generator, nie jest wymagana delegacja. Więc nie yield fromma sensu, chyba że twoje opakowanie zrobi coś innego generator-y.
ShadowRanger
14

return generator(list)robi co chcesz. Ale zauważ to

yield from generator(list)

byłoby równoważne, ale z możliwością uzyskania większej wartości po generatorwyczerpaniu. Na przykład:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
źródło
5
Uważam, że istnieje subtelna różnica pomiędzy yield fromi returnkiedy konsument generatora throwsjest w nim wyjątkiem - i innymi operacjami, na które ma wpływ ślad stosu.
WorldSEnder
9

W tym konkretnym przypadku następujące dwie instrukcje wydają się funkcjonalnie równoważne:

return generator(list)

i

yield from generator(list)

Później jest mniej więcej taki sam jak

for i in generator(list):
    yield i

returnWyrażenie zwraca generator, którego szukasz. A yield fromlubyield oświadczenie zamienia całą swoją funkcję w coś, co zwraca generator, który przechodzi przez jeden szukasz.

Z punktu widzenia użytkownika nie ma różnicy. Jednak wewnętrznie returnjest prawdopodobnie bardziej wydajny, ponieważ nie owija się generator(list)w zbędny generator pass-thru. Jeśli planujesz wykonać jakiekolwiek przetwarzanie elementów owiniętego generatora, użyj yieldoczywiście jakiejś formy .

Szalony fizyk
źródło
4

Ty byś returnto zrobił .

yielding * spowodowałoby generate_all()ocenę samego generatora i wywołanienext tego zewnętrznego generatora zwróciłoby wewnętrzny generator zwrócony przez pierwszą funkcję, czego nie chciałbyś.

* Nie licząc yield from

Carcigenicate
źródło