Come faccio a sapere se un generatore è vuoto dall'inizio?


146

C'è un modo semplice di testare se il generatore non ha oggetti, come peek, hasNext, isEmpty, qualcosa del genere?


Correggimi se sbaglio, ma se potessi creare una soluzione veramente generica per qualsiasi generatore, sarebbe l'equivalente di impostare punti di interruzione sulle dichiarazioni di rendimento e avere la possibilità di "fare un passo indietro". Significherebbe clonare il frame dello stack sui rendimenti e ripristinarli su StopIteration?

Bene, immagino che li ripristinino StopIteration o meno, ma almeno StopIteration ti direbbe che era vuoto. Sì, ho bisogno di dormire ...

4
Penso di sapere perché lo vuole. Se stai realizzando lo sviluppo web con modelli e passando il valore restituito a un modello come Cheetah o qualcosa del genere, l'elenco vuoto []è convenientemente Falsey, quindi puoi fare un controllo se lo fai e fare un comportamento speciale per qualcosa o niente. I generatori sono veri anche se non producono elementi.
jpsimons

Ecco il mio caso d'uso ... Sto usando glob.iglob("filepattern")un modello jolly fornito dall'utente e voglio avvisare l'utente se il modello non corrisponde a nessun file. Certo che posso aggirare questo problema in vari modi, ma è utile essere in grado di verificare chiaramente se l'iteratore è rimasto vuoto o no.
LarsH

Può essere utilizzare questa soluzione: stackoverflow.com/a/11467686/463758
Balki

Risposte:


53

La semplice risposta alla tua domanda: no, non esiste un modo semplice. Ci sono molte soluzioni alternative.

In realtà non dovrebbe esserci un modo semplice, a causa di cosa sono i generatori: un modo per produrre una sequenza di valori senza tenere la sequenza in memoria . Quindi non c'è attraversamento all'indietro.

Potresti scrivere una funzione has_next o magari schiaffeggiarla su un generatore come metodo con un decoratore di fantasia se lo desideri.


2
abbastanza giusto, ha senso. sapevo che non c'era modo di trovare la lunghezza di un generatore, ma pensavo che avrei potuto perdere un modo per scoprire se inizialmente avrebbe generato qualcosa.
Dan

1
Oh, e per riferimento, ho provato ad attuare il mio suggerimento "decoratore di fantasia". DIFFICILE. Apparentemente copy.deepcopy non funziona sui generatori.
David Berger,

47
Non sono sicuro di essere d'accordo con "non dovrebbe esserci un modo semplice". Ci sono molte astrazioni nell'informatica che sono progettate per produrre una sequenza di valori senza tenere la sequenza in memoria, ma che consentono al programmatore di chiedere se esiste un altro valore senza rimuoverlo dalla "coda" se esiste. Esiste una sola sbirciatina senza richiedere "traversata all'indietro". Questo non vuol dire che un design iteratore debba fornire tale funzionalità, ma è sicuramente utile. Forse stai obiettando sulla base del fatto che il primo valore potrebbe cambiare dopo la sbirciatina?
LarsH

9
Sto obiettando che un'implementazione tipica non calcola nemmeno un valore fino a quando non è necessaria. Si potrebbe forzare l'interfaccia a farlo, ma potrebbe non essere ottimale per implementazioni leggere.
David Berger,

6
@ S. Lott non è necessario generare l'intera sequenza per sapere se la sequenza è vuota o meno. Il valore di un elemento di archiviazione è sufficiente: vedi la mia risposta.
Mark Ransom,

99

Suggerimento:

def peek(iterable):
    try:
        first = next(iterable)
    except StopIteration:
        return None
    return first, itertools.chain([first], iterable)

Uso:

res = peek(mysequence)
if res is None:
    # sequence is empty.  Do stuff.
else:
    first, mysequence = res
    # Do something with first, maybe?
    # Then iterate over the sequence:
    for element in mysequence:
        # etc.

2
Non riesco proprio a restituire il primo elemento due volte return first, itertools.chain([first], rest).
njzk2,

6
@ njzk2 Stavo per un'operazione "peek" (da cui il nome della funzione). wiki "peek è un'operazione che restituisce il valore della parte superiore della raccolta senza rimuovere il valore dai dati"
John Fouhy,

Questo non funzionerà se il generatore è progettato per produrre Nessuno. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul,

4
@Paul Guarda attentamente i valori di ritorno. Se il generatore viene eseguito, ovvero non ritorna None, ma aumenta StopIteration, il risultato della funzione è None. Altrimenti, è una tupla, che non lo è None.
Finanzi la causa di Monica il

Questo mi ha aiutato molto con il mio progetto attuale. Ho trovato un esempio simile nel codice per il modulo di libreria standard 'mailbox.py' di python. This method is for backward compatibility only. def next(self): """Return the next message in a one-time iteration.""" if not hasattr(self, '_onetime_keys'): self._onetime_keys = self.iterkeys() while True: try: return self[next(self._onetime_keys)] except StopIteration: return None except KeyError: continue
peer

29

Un modo semplice è usare il parametro opzionale per next () che viene usato se il generatore è esaurito (o vuoto). Per esempio:

iterable = some_generator()

_exhausted = object()

if next(iterable, _exhausted) == _exhausted:
    print('generator is empty')

Modifica: corretto il problema sottolineato nel commento di mehtunguh.


1
No. Questo non è corretto per qualsiasi generatore in cui il primo valore prodotto non è vero.
mehtunguh,

7
Utilizzare un object()invece di classrenderlo una linea più breve: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

O sostituisci Nonema qualunque valore tu sappia non è nel tuo generatore.

Modifica : Sì, questo salterà 1 elemento nel generatore. Spesso, tuttavia, controllo se un generatore è vuoto solo a fini di convalida, quindi non lo uso davvero. O altrimenti faccio qualcosa del tipo:

def foo(self):
    if next(self.my_generator(), None) is None:
        raise Exception("Not initiated")

    for x in self.my_generator():
        ...

Cioè, funziona se il tuo generatore proviene da una funzione , come in generator().


4
Perché questa non è la risposta migliore? Nel caso in cui il generatore ritorni None?
Ha detto il

8
Probabilmente perché questo ti obbliga a consumare effettivamente il generatore invece di provare se è vuoto.
bfontaine,

3
È brutto perché nel momento in cui chiamerai il prossimo (generatore, Nessuno) salterai 1 oggetto se è disponibile
Nathan Do

Corretto, ti mancherà il primo elemento della tua gen e anche tu consumerai la tua gen piuttosto che testare se è vuoto.
AJ

12

L'approccio migliore, IMHO, sarebbe quello di evitare un test speciale. La maggior parte delle volte, l'uso di un generatore è il test:

thing_generated = False

# Nothing is lost here. if nothing is generated, 
# the for block is not executed. Often, that's the only check
# you need to do. This can be done in the course of doing
# the work you wanted to do anyway on the generated output.
for thing in my_generator():
    thing_generated = True
    do_work(thing)

Se ciò non è abbastanza buono, puoi comunque eseguire un test esplicito. A questo punto, thingconterrà l'ultimo valore generato. Se non è stato generato nulla, sarà indefinito, a meno che tu non abbia già definito la variabile. Potresti verificare il valore di thing, ma è un po 'inaffidabile. Invece, basta impostare un flag all'interno del blocco e controllarlo in seguito:

if not thing_generated:
    print "Avast, ye scurvy dog!"

3
Questa soluzione proverà a consumare l'intero generatore rendendolo inutilizzabile per generatori infiniti.
Viktor Stískala,

@ ViktorStískala: non vedo il tuo punto. Sarebbe sciocco verificare se un generatore infinito ha prodotto risultati.
Vezult

Volevo sottolineare che la tua soluzione potrebbe contenere interruzioni nel ciclo for, perché non stai elaborando gli altri risultati ed è inutile che vengano generati. range(10000000)è un generatore finito (Python 3), ma non è necessario esaminare tutti gli elementi per scoprire se genera qualcosa.
Viktor Stískala,

1
@ ViktorStískala: capito. Tuttavia, il mio punto è questo: in generale, si desidera effettivamente operare sull'uscita del generatore. Nel mio esempio, se non viene generato nulla, ora lo sai. Altrimenti, si opera sull'uscita generata come previsto: "L'uso del generatore è il test". Non sono necessari test speciali o consumare inutilmente l'uscita del generatore. Ho modificato la mia risposta per chiarire questo.
Vezult,

8

Odio per offrire una seconda soluzione, in particolare uno che non vorrei usare me stesso, ma, se proprio doveva fare questo e di non consumare il generatore, come in altre risposte:

def do_something_with_item(item):
    print item

empty_marker = object()

try:
     first_item = my_generator.next()     
except StopIteration:
     print 'The generator was empty'
     first_item = empty_marker

if first_item is not empty_marker:
    do_something_with_item(first_item)
    for item in my_generator:
        do_something_with_item(item)

Ora non mi piace davvero questa soluzione, perché credo che non sia così che devono essere usati i generatori.


4

Mi rendo conto che questo post ha 5 anni a questo punto, ma l'ho trovato mentre cercavo un modo idiomatico di farlo e non ho visto la mia soluzione pubblicata. Quindi per i posteri:

import itertools

def get_generator():
    """
    Returns (bool, generator) where bool is true iff the generator is not empty.
    """
    gen = (i for i in [0, 1, 2, 3, 4])
    a, b = itertools.tee(gen)
    try:
        a.next()
    except StopIteration:
        return (False, b)
    return (True, b)

Naturalmente, come sono sicuro che molti commentatori faranno notare, questo è confuso e funziona solo in determinate situazioni limitate (dove i generatori sono privi di effetti collaterali, per esempio). YMMV.


1
Questo chiamerà il gengeneratore una sola volta per ogni oggetto, quindi gli effetti collaterali non sono un problema troppo grave. Ma memorizzerà una copia di tutto ciò che è stato estratto dal generatore tramite b, ma non tramite a, quindi le implicazioni della memoria sono simili alla sola esecuzione list(gen)e verifica.
Matthias Fripp,

Ha due problemi. 1. Questo itertool può richiedere una significativa memorizzazione ausiliaria (a seconda della quantità di dati temporanei che devono essere memorizzati). In generale, se un iteratore utilizza la maggior parte o tutti i dati prima dell'avvio di un altro iteratore, è più veloce usare list () invece di tee (). 2. Gli iteratori a T non sono sicuri per i thread. Un RuntimeError può essere generato quando si utilizzano contemporaneamente iteratori restituiti dalla stessa chiamata tee (), anche se l'iterabile originale è thread-safe.
AJ

3

Ci scusiamo per l'approccio ovvio, ma il modo migliore sarebbe quello di fare:

for item in my_generator:
     print item

Ora hai rilevato che il generatore è vuoto mentre lo stai utilizzando. Naturalmente, l'oggetto non verrà mai visualizzato se il generatore è vuoto.

Questo potrebbe non adattarsi esattamente al tuo codice, ma questo è lo scopo del linguaggio del generatore: iterando, quindi forse potresti cambiare leggermente il tuo approccio o non usare affatto i generatori.


Oppure ... l'interrogante potrebbe fornire qualche suggerimento sul perché si dovrebbe provare a rilevare un generatore vuoto?
S.Lott

volevi dire "non verrà visualizzato nulla poiché il generatore è vuoto"?
SilentGhost,

S. Lott. Sono d'accordo. Non vedo perché. Ma penso che anche se ci fosse una ragione, il problema potrebbe essere meglio risolto per utilizzare invece ogni elemento.
Ali Afshar,

1
Questo non dice al programma se il generatore era vuoto.
Ethan Furman,

3

Tutto quello che devi fare per vedere se un generatore è vuoto è cercare di ottenere il risultato successivo. Naturalmente se non sei pronto per utilizzare quel risultato, devi memorizzarlo per restituirlo in seguito.

Ecco una classe wrapper che può essere aggiunta a un iteratore esistente per aggiungere un __nonzero__test, in modo da poter vedere se il generatore è vuoto con un semplice if. Probabilmente può anche essere trasformato in un decoratore.

class GenWrapper:
    def __init__(self, iter):
        self.source = iter
        self.stored = False

    def __iter__(self):
        return self

    def __nonzero__(self):
        if self.stored:
            return True
        try:
            self.value = next(self.source)
            self.stored = True
        except StopIteration:
            return False
        return True

    def __next__(self):  # use "next" (without underscores) for Python 2.x
        if self.stored:
            self.stored = False
            return self.value
        return next(self.source)

Ecco come lo useresti:

with open(filename, 'r') as f:
    f = GenWrapper(f)
    if f:
        print 'Not empty'
    else:
        print 'Empty'

Si noti che è possibile verificare la presenza di vuoto in qualsiasi momento, non solo all'inizio dell'iterazione.


Questo è diretto nella giusta direzione. Dovrebbe essere modificato per consentire una sbirciatina in anticipo quanto desideri, memorizzando tutti i risultati necessari. Idealmente, consentirebbe di spingere oggetti arbitrari sulla testa del flusso. Un iteratore pushable è un'astrazione molto utile che uso spesso.
sfkleach,

@sfkleach Non vedo la necessità di complicare questo per molteplici anticipazioni, è abbastanza utile così com'è e risponde alla domanda. Anche se questa è una vecchia domanda sta ancora ottenendo l'aspetto occasionale, quindi se vuoi lasciare la tua risposta qualcuno potrebbe trovarla utile.
Mark Ransom,

Mark ha perfettamente ragione sul fatto che la sua soluzione risponde alla domanda, che è il punto chiave. Avrei dovuto dirlo meglio. Intendevo dire che gli iteratori pushable con pushback illimitato sono un linguaggio che ho trovato estremamente utile e l'implementazione è probabilmente anche più semplice. Come suggerito, posterò il codice variante.
sfkleach

2

Su richiesta di Mark Ransom, ecco una classe che puoi usare per avvolgere qualsiasi iteratore in modo da poter sbirciare in avanti, reinserire i valori nello stream e controllare se sono vuoti. È un'idea semplice con una semplice implementazione che ho trovato molto utile in passato.

class Pushable:

    def __init__(self, iter):
        self.source = iter
        self.stored = []

    def __iter__(self):
        return self

    def __bool__(self):
        if self.stored:
            return True
        try:
            self.stored.append(next(self.source))
        except StopIteration:
            return False
        return True

    def push(self, value):
        self.stored.append(value)

    def peek(self):
        if self.stored:
            return self.stored[-1]
        value = next(self.source)
        self.stored.append(value)
        return value

    def __next__(self):
        if self.stored:
            return self.stored.pop()
        return next(self.source)

2

Sono appena caduto su questo thread e ho capito che mancava una risposta molto semplice e facile da leggere:

def is_empty(generator):
    for item in generator:
        return False
    return True

Se non si suppone di consumare alcun articolo, è necessario reiniettare il primo articolo nel generatore:

def is_empty_no_side_effects(generator):
    try:
        item = next(generator)
        def my_generator():
            yield item
            yield from generator
        return my_generator(), False
    except StopIteration:
        return (_ for _ in []), True

Esempio:

>>> g=(i for i in [])
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
True
>>> g=(i for i in range(10))
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
False
>>> list(g)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1
>>> gen = (i for i in [])
>>> next(gen)
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    next(gen)
StopIteration

Alla fine del generatore StopIterationviene sollevato, poiché nel tuo caso viene raggiunta immediatamente la fine, viene sollevata l'eccezione. Ma normalmente non dovresti controllare l'esistenza del prossimo valore.

un'altra cosa che puoi fare è:

>>> gen = (i for i in [])
>>> if not list(gen):
    print('empty generator')

2
Il che effettivamente consuma l'intero generatore. Purtroppo, non è chiaro dalla domanda se si tratti di un comportamento desiderabile o indesiderabile.
S.Lott

come qualsiasi altro modo di "toccare" il generatore, suppongo.
SilentGhost,

Mi rendo conto che questo è vecchio, ma usare 'list ()' non può essere il modo migliore, se l'elenco generato non è vuoto ma in realtà grande, questo è inutilmente dispendioso
Chris_Rands

1

Se devi sapere prima di utilizzare il generatore, allora no, non esiste un modo semplice. Se puoi aspettare fino a quando non hai usato il generatore, c'è un modo semplice:

was_empty = True

for some_item in some_generator:
    was_empty = False
    do_something_with(some_item)

if was_empty:
    handle_already_empty_generator_case()

1

Avvolgi semplicemente il generatore con itertools.chain , metti qualcosa che rappresenterà la fine dell'iterabile come secondo iterabile, quindi controlla semplicemente quello.

Ex:

import itertools

g = some_iterable
eog = object()
wrap_g = itertools.chain(g, [eog])

Ora tutto ciò che rimane è verificare il valore che abbiamo aggiunto alla fine dell'iterabile, quando lo leggi allora significherà la fine

for value in wrap_g:
    if value == eog: # DING DING! We just found the last element of the iterable
        pass # Do something

Utilizzare eog = object()invece di supporre che float('-inf')non si verificherà mai nell'iterabile.
bfontaine,

@bfontaine Buona idea
smac89

1

Nel mio caso avevo bisogno di sapere se un host di generatori era popolato prima di passarlo a una funzione, che univa gli elementi, cioè zip(...). La soluzione è simile, ma abbastanza diversa, dalla risposta accettata:

Definizione:

def has_items(iterable):
    try:
        return True, itertools.chain([next(iterable)], iterable)
    except StopIteration:
        return False, []

Uso:

def filter_empty(iterables):
    for iterable in iterables:
        itr_has_items, iterable = has_items(iterable)
        if itr_has_items:
            yield iterable


def merge_iterables(iterables):
    populated_iterables = filter_empty(iterables)
    for items in zip(*populated_iterables):
        # Use items for each "slice"

Il mio problema particolare ha la proprietà che gli iterabili sono vuoti o hanno esattamente lo stesso numero di voci.


1

Ho trovato solo questa soluzione funzionante anche per iterazioni vuote.

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    try:
        next(a)
    except StopIteration:
        return True, b
    return False, b

is_empty, generator = is_generator_empty(generator)

O se non si desidera utilizzare l'eccezione per questo, provare a utilizzare

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    for item in a:
        return False, b
    return True, b

is_empty, generator = is_generator_empty(generator)

Nella soluzione contrassegnata non è possibile usarlo per generatori vuoti come

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Ecco il mio semplice approccio che uso per continuare a restituire un iteratore mentre controllo se qualcosa è stato prodotto Controllo solo se il ciclo funziona:

        n = 0
        for key, value in iterator:
            n+=1
            yield key, value
        if n == 0:
            print ("nothing found in iterator)
            break

0

Ecco un semplice decoratore che avvolge il generatore, quindi restituisce None se vuoto. Questo può essere utile se il tuo codice deve sapere se il generatore produrrà qualcosa prima di eseguirne il ciclo.

def generator_or_none(func):
    """Wrap a generator function, returning None if it's empty. """

    def inner(*args, **kwargs):
        # peek at the first item; return None if it doesn't exist
        try:
            next(func(*args, **kwargs))
        except StopIteration:
            return None

        # return original generator otherwise first item will be missing
        return func(*args, **kwargs)

    return inner

Uso:

import random

@generator_or_none
def random_length_generator():
    for i in range(random.randint(0, 10)):
        yield i

gen = random_length_generator()
if gen is None:
    print('Generator is empty')

Un esempio in cui questo è utile è nel codice di template - ovvero jinja2

{% if content_generator %}
  <section>
    <h4>Section title</h4>
    {% for item in content_generator %}
      {{ item }}
    {% endfor %
  </section>
{% endif %}

Questo chiama due volte la funzione generatore, quindi due volte il costo di avvio del generatore. Ciò potrebbe essere sostanziale se, ad esempio, la funzione del generatore è una query del database.
Ian Goldby,

0

usando islice devi solo controllare fino alla prima iterazione per scoprire se è vuoto.

da itertools import islice

def isempty (iterable):
    lista di restituzione (islice (iterable, 1)) == []


Siamo spiacenti, questa è una lettura consumativa ... Devo fare il tentativo / catturare con StopIteration
Quin

0

Che ne dici di usare any ()? Lo uso con i generatori e funziona benissimo. Qui c'è un ragazzo che spiega un po 'di questo


2
Non possiamo usare "any ()" per tutto il generatore. Ho appena provato a usarlo con un generatore che contiene più frame di dati. Ho ricevuto questo messaggio "Il valore di verità di un DataFrame è ambiguo". on any (my_generator_of_df)
probitaille

any(generator)funziona quando sai che il generatore genererà valori su cui è possibile eseguire il cast bool: i tipi di dati di base (ad es. int, stringa) funzionano. any(generator)sarà Falso quando il generatore è vuoto o quando il generatore ha solo valori falsi, ad esempio se un generatore genererà 0, "(stringa vuota) e Falso, sarà comunque Falso. Questo potrebbe o non potrebbe essere il comportamento previsto, basta che tu ne sia consapevole :)
Daniel,

0

Utilizzare la funzione peek in cytoolz.

from cytoolz import peek
from typing import Tuple, Iterable

def is_empty_iterator(g: Iterable) -> Tuple[Iterable, bool]:
    try:
        _, g = peek(g)
        return g, False
    except StopIteration:
        return g, True

L'iteratore restituito da questa funzione sarà equivalente a quello originale passato come argomento.


-2

L'ho risolto usando la funzione somma. Vedi sotto per un esempio che ho usato con glob.iglob (che restituisce un generatore).

def isEmpty():
    files = glob.iglob(search)
    if sum(1 for _ in files):
        return True
    return False

* Questo probabilmente non funzionerà per generatori ENORMI ma dovrebbe funzionare bene per elenchi più piccoli

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.