Python Empty Generator-Funktion

97

In Python kann eine Iteratorfunktion leicht definiert werden, indem das Schlüsselwort yield in den Funktionskörper eingefügt wird, z.

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

Wie kann ich eine Generatorfunktion definieren, die keinen Wert liefert (generiert 0 Werte)? Der folgende Code funktioniert nicht, da Python nicht wissen kann, dass es sich um einen Generator und nicht um eine normale Funktion handeln soll:

def empty():
    pass

Ich könnte so etwas tun

def empty():
    if False:
        yield None

Das wäre aber sehr hässlich. Gibt es eine gute Möglichkeit, eine leere Iteratorfunktion zu realisieren?

Konstantin Weitz
quelle

Antworten:

132

Sie können returneinmal in einem Generator verwenden; Es stoppt die Iteration, ohne etwas zu ergeben, und bietet somit eine explizite Alternative dazu, dass die Funktion keinen Gültigkeitsbereich mehr hat. Verwenden Sie also yield, um die Funktion in einen Generator umzuwandeln, aber gehen Sie voran, returnum den Generator zu beenden, bevor Sie etwas liefern.

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

Ich bin mir nicht sicher, ob es so viel besser ist als das, was Sie haben - es ersetzt nur eine No-Op- ifAnweisung durch eine No-Op- yieldAnweisung. Aber es ist idiomatischer. Beachten Sie, dass nur die Verwendung yieldnicht funktioniert.

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

Warum nicht einfach benutzen iter(())?

Diese Frage bezieht sich speziell auf eine leere Generatorfunktion . Aus diesem Grund verstehe ich es eher als eine Frage zur internen Konsistenz der Python-Syntax als als eine Frage nach dem besten Weg, einen leeren Iterator im Allgemeinen zu erstellen.

Wenn es bei der Frage tatsächlich darum geht, wie ein leerer Iterator am besten erstellt werden kann, können Sie Zectbumo zustimmen , iter(())stattdessen einen zu verwenden. Es ist jedoch wichtig zu beachten, dass iter(())keine Funktion zurückgegeben wird! Es gibt direkt eine leere iterable zurück. Angenommen, Sie arbeiten mit einer API, die eine aufrufbare Datei erwartet, die eine iterable zurückgibt . Sie müssen so etwas tun:

def empty():
    return iter(())

(Gutschrift sollte an Unutbu gehen, um die erste richtige Version dieser Antwort zu geben.)

Vielleicht finden Sie das oben Genannte klarer, aber ich kann mir Situationen vorstellen, in denen es weniger klar wäre. Betrachten Sie dieses Beispiel einer langen Liste von (erfundenen) Generatorfunktionsdefinitionen:

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Am Ende dieser langen Liste würde ich lieber etwas mit einem yielddarin sehen, wie folgt:

def empty():
    return
    yield

oder in Python 3.3 und höher (wie von DSM vorgeschlagen ):

def empty():
    yield from ()

Das Vorhandensein des yieldSchlüsselworts macht auf den ersten Blick deutlich, dass dies genau wie alle anderen nur eine weitere Generatorfunktion ist. Es dauert etwas länger, bis festgestellt wird, dass die iter(())Version dasselbe tut.

Es ist ein subtiler Unterschied, aber ich denke ehrlich, dass die yieldbasierten Funktionen besser lesbar und wartbar sind.

senderle
quelle
Dies ist in der Tat besser als, if False: yieldaber immer noch etwas verwirrend für Leute, die dieses Muster nicht kennen
Konstantin Weitz
1
Ew, etwas nach der Rückkehr? Ich habe so etwas erwartet itertools.empty().
Grault
1
@Jesdisciple returnbedeutet etwas anderes in Generatoren. Es ist eher so break.
Absender
Ich mag diese Lösung, weil sie (relativ) prägnant ist und keine zusätzliche Arbeit wie im Vergleich zu leistet False.
Pi Marillion
Die Antwort von Unutbu ist keine "echte Generatorfunktion", wie Sie erwähnt haben, da sie einen Iterator zurückgibt.
Zectbumo
72
iter(())

Sie benötigen keinen Generator. Komm schon Leute!

Zectbumo
quelle
3
Diese Antwort gefällt mir auf jeden Fall am besten. Es ist schnell, einfach zu schreiben, schnell in der Ausführung und für mich ansprechender als iter([])die einfache Tatsache, dass ()eine konstante Zeit []jedes Mal, wenn es aufgerufen wird, ein neues Listenobjekt im Speicher instanziieren kann.
Mumbleskates
Für mich scheint dies auch die eleganteste Lösung zu sein.
Matthias CM Troffaes
2
Wenn ich diesen Thread noch einmal durcharbeite, muss ich darauf hinweisen, dass Sie, wenn Sie einen echten Ersatz für eine Generatorfunktion wünschen, so etwas wie empty = lambda: iter(())oder schreiben müssten def empty(): return iter(()).
senderle
Wenn Sie einen Generator haben müssen, können Sie auch (_ für _ in ()) verwenden, wie andere vorgeschlagen haben
Zectbumo
1
@Zectbumo, das ist immer noch kein Generator - Funktion . Es ist nur ein Generator. Eine Generatorfunktion gibt bei jedem Aufruf einen neuen Generator zurück.
senderle
50

Python 3.3 (weil ich einen yield fromKick habe und weil @senderle meinen ersten Gedanken gestohlen hat):

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

Aber ich muss zugeben, es fällt mir schwer, einen Anwendungsfall dafür zu finden, für den es nicht so gut funktioniert iter([])oder (x)range(0)nicht.

DSM
quelle
Diese Syntax gefällt mir sehr gut. Ertrag von ist fantastisch!
Konstantin Weitz
2
Ich denke, dass dies für einen Anfänger viel besser lesbar ist als entweder return; yieldoder if False: yield None.
Abarnert
1
Dies ist die eleganteste Lösung
Maksym Ganenko
"Aber ich muss zugeben, es fällt mir schwer, einen Anwendungsfall dafür zu finden, für den ich nicht gleich gut funktionieren würde iter([])oder (x)range(0)nicht." -> Ich bin mir nicht sicher, was es (x)range(0)ist, aber ein Anwendungsfall kann eine Methode sein, die mit einem vollständigen Generator in einigen der erbenden Klassen überschrieben werden soll. Aus Konsistenzgründen möchten Sie, dass sogar der Basisgenerator, von dem andere erben, einen Generator zurückgibt, genau wie diejenigen, die ihn überschreiben.
Vedran Šego
18

Eine weitere Option ist:

(_ for _ in ())
Ben Reynwar
quelle
3
Das gefällt mir auch. Darf ich ein leeres Tupel anstelle einer leeren Liste vorschlagen? Auf diese Weise erhalten Sie eine konstante Faltung .
senderle
1
Vielen Dank. Das wusste ich nicht.
Ben Reynwar
4

Muss es eine Generatorfunktion sein? Wenn nicht, wie wäre es?

def f():
    return iter([])
unutbu
quelle
3

Die "Standard" -Methode zum Erstellen eines leeren Iterators scheint iter ([]) zu sein. Ich schlug vor, [] zum Standardargument für iter () zu machen. Dies wurde mit guten Argumenten abgelehnt, siehe http://bugs.python.org/issue25215 - Jurjen

Jurjen Bos
quelle
1

Wie @senderle sagte, benutze dies:

def empty():
    return
    yield

Ich schreibe diese Antwort hauptsächlich, um eine weitere Rechtfertigung dafür zu teilen.

Ein Grund für die Wahl dieser Lösung gegenüber den anderen ist, dass sie für den Dolmetscher optimal ist.

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

Wie wir sehen können, empty_returnhat der genau den gleichen Bytecode wie eine reguläre leere Funktion; Der Rest führt eine Reihe anderer Vorgänge aus, die das Verhalten ohnehin nicht ändern. Der einzige Unterschied zwischen empty_returnund noopbesteht darin, dass bei ersteren das Generatorflag gesetzt ist:

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

Die Stärke dieses Arguments hängt natürlich stark von der jeweiligen Implementierung von Python ab. Ein ausreichend intelligenter alternativer Interpreter kann feststellen, dass die anderen Operationen nichts Nützliches darstellen, und sie optimieren. Selbst wenn solche Optimierungen vorhanden sind, muss der Interpreter Zeit damit verbringen, sie auszuführen und sich vor dem Bruch von Optimierungsannahmen zu schützen, z. B. iterwenn die Kennung im globalen Bereich auf etwas anderes zurückgesetzt wird (obwohl dies höchstwahrscheinlich auf einen Fehler hinweisen würde, wenn dies der Fall ist tatsächlich passiert). Im Falle von empty_returngibt es einfach nichts zu optimieren, so dass selbst der relativ naive CPython keine Zeit mit falschen Operationen verschwendet.

user3840170
quelle
0
generator = (item for item in [])

quelle