Funkcja pustego generatora Pythona


104

W Pythonie można łatwo zdefiniować funkcję iteratora, umieszczając słowo kluczowe yield w treści funkcji, na przykład:

def gen():
    for i in range(100):
        yield i

Jak mogę zdefiniować funkcję generatora, która nie daje żadnej wartości (generuje wartości 0), poniższy kod nie działa, ponieważ Python nie może wiedzieć, że ma to być generator, a nie normalna funkcja:

def empty():
    pass

Mógłbym zrobić coś takiego

def empty():
    if False:
        yield None

Ale to byłoby bardzo brzydkie. Czy jest jakiś fajny sposób na zrealizowanie pustej funkcji iteratora?

Odpowiedzi:


139

Możesz użyć returnraz w generatorze; zatrzymuje iterację bez zwracania niczego, a tym samym zapewnia jawną alternatywę dla pozostawienia funkcji poza zakresem. Użyj więc, yieldaby zamienić funkcję w generator, ale poprzedź ją, returnaby zakończyć generator przed uzyskaniem czegokolwiek.

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Nie jestem pewien, czy jest o wiele lepszy niż to, co masz - po prostu zastępuje ifoświadczenie „no-op” yieldoświadczeniem „ no-op” . Ale to jest bardziej idiomatyczne. Zwróć uwagę, że samo użycie yieldnie działa.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Dlaczego po prostu nie użyć iter(())?

To pytanie dotyczy konkretnie pustej funkcji generatora . Z tego powodu traktuję to jako pytanie o wewnętrzną spójność składni Pythona, a nie o najlepszy sposób tworzenia pustego iteratora w ogóle.

Jeśli faktycznie pytanie dotyczy najlepszego sposobu utworzenia pustego iteratora, możesz zgodzić się z Zectbumo na użycie iter(())zamiast tego. Należy jednak zauważyć, że iter(())nie zwraca funkcji! Bezpośrednio zwraca pustą iterowalną. Załóżmy, że pracujesz z interfejsem API, który oczekuje wywołania, które zwraca iterowalną wartość za każdym razem, gdy jest wywoływana, tak jak zwykła funkcja generatora. Musisz zrobić coś takiego:

def empty():
    return iter(())

( Podziękowania należy się Unutbu za podanie pierwszej poprawnej wersji tej odpowiedzi).

Powyższe może być dla ciebie jaśniejsze, ale mogę sobie wyobrazić sytuacje, w których byłoby to mniej jasne. Rozważmy następujący przykład długiej listy (wymyślonych) definicji funkcji generatora:

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Na końcu tej długiej listy wolałbym zobaczyć coś, co zawiera znak yield, na przykład:

def empty():
    return
    yield

lub w Pythonie 3.3 i nowszych (zgodnie z sugestią DSM ):

def empty():
    yield from ()

Obecność yieldsłowa kluczowego sprawia, że ​​na pierwszy rzut oka widać, że jest to tylko kolejna funkcja generatora, dokładnie taka, jak wszystkie inne. Potrzeba trochę więcej czasu, aby zobaczyć, że iter(())wersja robi to samo.

To subtelna różnica, ale szczerze uważam, że yieldfunkcje oparte na systemie są bardziej czytelne i łatwiejsze w utrzymaniu.

Zobacz także tę świetną odpowiedź od użytkownika 3840170, która używa, disaby pokazać inny powód, dla którego takie podejście jest lepsze: po kompilacji emituje najmniej instrukcji.


To jest rzeczywiście lepsze niż, if False: yieldale wciąż trochę mylące dla ludzi, którzy nie znają tego wzoru
Konstantin Weitz,

1
Ew, coś po powrocie? Spodziewałem się czegoś takiego itertools.empty().
Grault

1
@Jesdisciple, cóż, returnoznacza coś innego wewnątrz generatorów. To bardziej przypomina break.
senderle

Podoba mi się to rozwiązanie, ponieważ jest (stosunkowo) zwięzłe i nie wymaga dodatkowej pracy, takiej jak porównanie False.
Pi Marillion

Odpowiedź Unutbu nie jest „prawdziwą funkcją generatora”, jak wspomniałeś, ponieważ zwraca iterator.
Zectbumo

71
iter(())

Nie potrzebujesz generatora. Dalej chłopaki!


3
Zdecydowanie najbardziej podoba mi się ta odpowiedź. Jest szybki, łatwy do napisania, szybki w wykonaniu i bardziej pociągający dla mnie niż iter([])prosty fakt, że ()jest stały, a przy każdym []wywołaniu może utworzyć w pamięci nowy obiekt listy.
Mumbleskates

2
Wracając do tego wątku, czuję się zmuszony wskazać, że jeśli chcesz prawdziwego zamiennika typu drop-in dla funkcji generatora, napisz coś takiego jak empty = lambda: iter(())lub def empty(): return iter(()).
senderle

Jeśli musisz mieć generator, możesz równie dobrze użyć (_ for _ in ()), jak sugerowali inni
Zectbumo

1
@Zectbumo, to wciąż nie jest funkcja generatora . To tylko generator. Funkcja generatora zwraca nowy generator za każdym razem, gdy jest wywoływana.
senderle

1
Zwraca a tuple_iteratorzamiast a generator. Jeśli masz przypadek, w którym twój generator nie musi nic zwracać, nie używaj tej odpowiedzi.
Boris

54

Python 3.3 (ponieważ jestem na yield fromfali i ponieważ @senderle ukradł moją pierwszą myśl):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Muszę jednak przyznać, że trudno jest mi wymyślić przypadek użycia, który iter([])lub (x)range(0)nie działałby równie dobrze.


Bardzo podoba mi się ta składnia. plon jest niesamowity!
Konstantin Weitz

2
Myślę, że jest to znacznie bardziej czytelne dla nowicjusza niż jedno return; yieldlub drugie if False: yield None.
abarnert

1
To najbardziej eleganckie rozwiązanie
Maksym Ganenko

„Muszę jednak przyznać, że trudno jest mi znaleźć taki przypadek użycia, w którym iter([])lub (x)range(0)nie działałby równie dobrze”. -> Nie jestem pewien, co to (x)range(0)jest, ale przypadkiem użycia może być metoda, która ma zostać zastąpiona przez pełny generator w niektórych klasach dziedziczących. Aby zachować spójność, chciałbyś, aby nawet podstawowy, z którego dziedziczą inni, zwracał generator, taki jak ten, który go zastępuje.
Vedran Šego

18

Inną opcją jest:

(_ for _ in ())

W odróżnieniu od innych opcji, Pycharm uważa to za zgodne ze standardowymi podpowiedziami typu używanymi w generatorach, jakGenerator[str, Any, None]
Michał Jabłoński

3

Jak powiedział @senderle, użyj tego:

def empty():
    return
    yield

Piszę tę odpowiedź głównie po to, aby podzielić się innym uzasadnieniem.

Jednym z powodów wyboru tego rozwiązania spośród innych jest to, że jest ono optymalne z punktu widzenia tłumacza.

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Jak widać, empty_returnma dokładnie ten sam kod bajtowy, co zwykła pusta funkcja; reszta wykonuje szereg innych operacji, które i tak nie zmieniają zachowania. Jedyna różnica między empty_returni nooppolega na tym, że ten pierwszy ma ustawioną flagę generatora:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Oczywiście siła tego argumentu jest bardzo zależna od konkretnej implementacji używanego Pythona; dostatecznie inteligentny interpretator alternatywny może zauważyć, że inne operacje nie są pożyteczne i zoptymalizować je. Jednak nawet jeśli takie optymalizacje są obecne, wymagają one od tłumacza poświęcenia czasu na ich wykonanie i zabezpieczenia się przed złamaniem założeń optymalizacyjnych, takich jak iterodbicie identyfikatora w zakresie globalnym do czegoś innego (nawet jeśli najprawdopodobniej wskazywałoby to na błąd, gdyby faktycznie się wydarzyło). W tym przypadku empty_returnpo prostu nie ma co optymalizować, więc nawet stosunkowo naiwny CPython nie będzie tracił czasu na żadne fałszywe operacje.


Och, fajnie. Czy możesz dodać wyniki yield from ()również dla? (Zobacz odpowiedź DSM .)
senderle

3

Czy musi to być funkcja generatora? Jeśli nie, to co powiesz

def f():
    return iter(())


0
generator = (item for item in [])

0

Chcę podać przykład oparty na klasach, ponieważ nie mieliśmy jeszcze żadnych sugestii. Jest to wywoływalny iterator, który nie generuje żadnych elementów. Uważam, że jest to prosty i opisowy sposób rozwiązania problemu.

class EmptyGenerator:
    def __iter__(self):
        return self
    def __next__(self):
        raise StopIteration

>>> list(EmptyGenerator())
[]

Czy możesz dodać wyjaśnienie, dlaczego / jak to działa, aby rozwiązać problem PO?
SherylHohman

Nie wysyłaj tylko kodu jako odpowiedzi, ale także wyjaśnij, co robi twój kod i jak rozwiązuje problem. Odpowiedzi z wyjaśnieniem są zwykle bardziej pomocne i lepszej jakości oraz częściej przyciągają głosy za.
SherylHohman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.