Funzione generatore vuoto Python


98

In python, si può facilmente definire una funzione iteratore, inserendo la parola chiave yield nel corpo della funzione, come ad esempio:

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

Come posso definire una funzione generatore che non produce valore (genera valori 0), il seguente codice non funziona, poiché python non può sapere che dovrebbe essere un generatore e non una funzione normale:

def empty():
    pass

Potrei fare qualcosa di simile

def empty():
    if False:
        yield None

Ma sarebbe molto brutto. C'è un modo carino per realizzare una funzione iteratore vuota?

Risposte:


132

Puoi usare returnuna volta in un generatore; interrompe l'iterazione senza produrre nulla e quindi fornisce un'alternativa esplicita per lasciare che la funzione esca dall'ambito. Quindi usa yieldper trasformare la funzione in un generatore, ma precedilo returnper terminare il generatore prima di cedere qualsiasi cosa.

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

Non sono sicuro che sia molto meglio di quello che hai - sostituisce semplicemente ifun'istruzione no-op con un'istruzione no-op yield. Ma è più idiomatico. Nota che il semplice utilizzo yieldnon funziona.

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

Perché non basta usare iter(())?

Questa domanda chiede specificamente una funzione di generatore vuoto . Per questo motivo, ritengo che sia una domanda sulla coerenza interna della sintassi di Python, piuttosto che una domanda sul modo migliore per creare un iteratore vuoto in generale.

Se la domanda riguarda effettivamente il modo migliore per creare un iteratore vuoto, potresti essere d'accordo con Zectbumo sull'utilizzo iter(()). Tuttavia, è importante osservare che iter(())non restituisce una funzione! Restituisce direttamente un iterabile vuoto. Supponi di lavorare con un'API che si aspetta un chiamabile che restituisce un iterabile. Dovrai fare qualcosa del genere:

def empty():
    return iter(())

(Il merito dovrebbe andare a Unutbu per aver fornito la prima versione corretta di questa risposta.)

Ora, potresti trovare quanto sopra più chiaro, ma posso immaginare situazioni in cui sarebbe meno chiaro. Considera questo esempio di un lungo elenco di definizioni di funzioni del generatore (inventate):

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Alla fine di quella lunga lista, preferirei vedere qualcosa con yielddentro, come questo:

def empty():
    return
    yield

oppure, in Python 3.3 e versioni successive (come suggerito da DSM ), questo:

def empty():
    yield from ()

La presenza della yieldparola chiave fa capire al più breve sguardo che questa è solo un'altra funzione del generatore, esattamente come tutte le altre. Ci vuole un po 'più di tempo per vedere che la iter(())versione sta facendo la stessa cosa.

È una sottile differenza, ma onestamente penso che le yieldfunzioni basate su siano più leggibili e gestibili.


Questo è davvero meglio, if False: yieldma ancora un po 'confuso per le persone che non conoscono questo schema
Konstantin Weitz

1
Ew, qualcosa dopo il ritorno? Mi aspettavo qualcosa di simile itertools.empty().
Grault

1
@ Jesdisciple, beh, returnsignifica qualcosa di diverso all'interno dei generatori. È più simile break.
mittente

Mi piace questa soluzione perché è (relativamente) concisa e non fa alcun lavoro extra come il confronto con False.
Pi Marillion

La risposta di Unutbu non è una "vera funzione generatore" come hai menzionato poiché restituisce un iteratore.
Zectbumo

72
iter(())

Non hai bisogno di un generatore. Andiamo ragazzi!


3
Questa risposta mi piace decisamente di più. È veloce, facile da scrivere, veloce nell'esecuzione e più attraente per me che iter([])per il semplice fatto che ()è un tempo costante che []può istanziare un nuovo oggetto elenco in memoria ogni volta che viene chiamato.
Mumbleskates

Anche a me questa sembra la soluzione più elegante.
Matthias CM Troffaes

2
Ripercorrendo questo thread, mi sento obbligato a sottolineare che se vuoi un vero sostituto drop-in per una funzione di generatore, dovresti scrivere qualcosa come empty = lambda: iter(())o def empty(): return iter(()).
senderle

Se devi avere un generatore, potresti anche usare (_ per _ in ()) come altri hanno suggerito
Zectbumo

1
@ Zectbumo, non è ancora una funzione del generatore . È solo un generatore. Una funzione del generatore restituisce un nuovo generatore ogni volta che viene chiamata.
mittente

50

Python 3.3 (perché sono su un yield fromcalcio, e perché @senderle ha rubato il mio primo pensiero):

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

Ma devo ammettere che ho difficoltà a trovare un caso d'uso per questo per il quale iter([])o (x)range(0)non funzionerebbe altrettanto bene.


Mi piace molto questa sintassi. resa da è impressionante!
Konstantin Weitz

2
Penso che questo sia molto più leggibile per un principiante rispetto a return; yieldo if False: yield None.
abarnert

1
Questa è la soluzione più elegante
Maksym Ganenko

"Ma devo ammetterlo, sto avendo difficoltà a trovare un caso d'uso per questo per il quale iter([])o (x)range(0)non funzionerebbe altrettanto bene". -> Non sono sicuro di cosa (x)range(0)sia, ma un caso d'uso può essere un metodo che deve essere sovrascritto con un generatore completo in alcune delle classi che ereditano. Per motivi di coerenza, vorresti che anche quello di base, da cui gli altri ereditano, restituisse un generatore proprio come quelli che lo sovrascrivono.
Vedran Šego

19

Un'altra opzione è:

(_ for _ in ())

3
Anche questo mi piace. Posso suggerire una tupla vuota invece di una lista vuota? In questo modo ottieni una piegatura costante .
senderle

1
Grazie. Non lo sapevo.
Ben Reynwar

A differenza di altre opzioni, Pycharm ritiene che ciò sia coerente con i suggerimenti di tipo standard utilizzati per i generatori, comeGenerator[str, Any, None]
Michał Jabłoński

4

Deve essere una funzione di generatore? In caso contrario, che ne dici

def f():
    return iter([])

3

Il modo "standard" per creare un iteratore vuoto sembra essere iter ([]). Ho suggerito di rendere [] l'argomento predefinito di iter (); questo è stato rifiutato con buoni argomenti, vedere http://bugs.python.org/issue25215 - Jurjen


1

Come ha detto @senderle, usa questo:

def empty():
    return
    yield

Sto scrivendo questa risposta principalmente per condividere un'altra giustificazione.

Uno dei motivi per scegliere questa soluzione rispetto alle altre è che è ottimale per l'interprete.

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

Come possiamo vedere, empty_returnha esattamente lo stesso bytecode di una normale funzione vuota; gli altri eseguono una serie di altre operazioni che non cambiano comunque il comportamento. L'unica differenza tra empty_returne noopè che il primo ha il flag del generatore impostato:

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

Naturalmente, la forza di questo argomento dipende molto dalla particolare implementazione di Python in uso; un interprete alternativo sufficientemente intelligente può notare che le altre operazioni non equivalgono a nulla di utile e ottimizzarle. Tuttavia, anche se tali ottimizzazioni sono presenti, richiedono all'interprete di dedicare del tempo a eseguirle e di salvaguardarsi dall'infrangere i presupposti di ottimizzazione, come l' iteridentificatore a livello globale che viene rimbalzato su qualcos'altro (anche se molto probabilmente indicherebbe un bug se realmente accaduto). Nel caso in cui empty_returnnon c'è semplicemente nulla da ottimizzare, quindi anche il CPython relativamente ingenuo non perderà tempo in operazioni spurie.


0
generator = (item for item in [])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.