¿Cómo sé si un generador está vacío desde el principio?


146

¿Hay una manera sencilla de comprobar si el generador no tiene elementos, como peek, hasNext, isEmpty, algo por el estilo?


Corríjame si me equivoco, pero si pudiera hacer una solución realmente genérica para cualquier generador, sería el equivalente a establecer puntos de interrupción en las declaraciones de rendimiento y tener la capacidad de "retroceder". ¿Eso significaría clonar el marco de la pila en rendimientos y restaurarlos en StopIteration?

Bueno, supongo que restaurarlos StopIteration o no, pero al menos StopIteration te diría que estaba vacío. Si Necesito sueño ...

44
Creo que sé por qué quiere esto. Si está haciendo desarrollo web con plantillas y está pasando el valor de retorno a una plantilla como Cheetah o algo así, la lista vacía []es convenientemente Falsey para que pueda hacer un chequeo y hacer un comportamiento especial para algo o nada. Los generadores son verdaderos incluso si no producen elementos.
jpsimons

Aquí está mi caso de uso ... Estoy usando glob.iglob("filepattern")un patrón comodín proporcionado por el usuario, y quiero advertir al usuario si el patrón no coincide con ningún archivo. Claro que puedo solucionar esto de varias maneras, pero es útil poder probar limpiamente si el iterador se quedó vacío o no.
LarsH

Puede usar esta solución: stackoverflow.com/a/11467686/463758
balki

Respuestas:


53

La respuesta simple a su pregunta: no, no hay una manera simple. Hay muchas soluciones alternativas.

Realmente no debería haber una manera simple, debido a lo que son los generadores: una forma de generar una secuencia de valores sin mantener la secuencia en la memoria . Por lo tanto, no hay recorrido hacia atrás.

Si lo desea, puede escribir una función has_next o incluso aplicarla a un generador como método con un elegante decorador.


2
bastante justo, eso tiene sentido. Sabía que no había forma de encontrar la longitud de un generador, pero pensé que podría haber perdido una forma de encontrarlo si inicialmente iba a generar algo.
Dan

1
Ah, y como referencia, intenté implementar mi propia sugerencia de "decorador elegante". DIFÍCIL. Aparentemente copy.deepcopy no funciona en generadores.
David Berger

47
No estoy seguro de poder estar de acuerdo con "no debería haber una manera simple". Hay muchas abstracciones en informática que están diseñadas para generar una secuencia de valores sin mantener la secuencia en la memoria, pero que le permiten al programador preguntar si hay otro valor sin eliminarlo de la "cola" si lo hay. Existe la posibilidad de mirar hacia adelante sin necesidad de "atravesar hacia atrás". Eso no quiere decir que un diseño de iterador deba proporcionar dicha característica, pero seguro que es útil. ¿Tal vez estás objetando porque el primer valor puede cambiar después del vistazo?
LarsH

9
Estoy objetando porque una implementación típica ni siquiera calcula un valor hasta que se necesita. Se podría forzar a la interfaz a hacer esto, pero eso podría ser subóptimo para implementaciones livianas.
David Berger

66
@ S.Lott no necesita generar la secuencia completa para saber si la secuencia está vacía o no. El valor de almacenamiento de un elemento es suficiente; vea mi respuesta.
Mark Ransom

98

Sugerencia:

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
No entiendo el punto de devolver el primer elemento dos veces return first, itertools.chain([first], rest).
njzk2

66
@ njzk2 Iba para una operación de "vistazo" (de ahí el nombre de la función). wiki "peek es una operación que devuelve el valor de la parte superior de la colección sin eliminar el valor de los datos"
John Fouhy

Esto no funcionará si el generador está diseñado para no producir ninguno. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

44
@Paul Mira los valores de retorno de cerca. Si el generador está listo, es decir, no regresa None, sino que sube StopIteration, el resultado de la función es None. De lo contrario, es una tupla, que no lo es None.
Financia la demanda de Mónica el

Esto me ayudó mucho con mi proyecto actual. Encontré un ejemplo similar en el código para el módulo de biblioteca estándar de python 'buzón.py'. 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
igual

29

Una manera simple es usar el parámetro opcional para next () que se usa si el generador está agotado (o vacío). Por ejemplo:

iterable = some_generator()

_exhausted = object()

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

Editar: corrigió el problema señalado en el comentario de mehtunguh.


1
No. Esto es incorrecto para cualquier generador en el que el primer valor obtenido sea falso.
mehtunguh

77
Use un en object()lugar de classpara hacerlo una línea más corta _exhausted = object():; if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

O reemplazar None, pero cualquier valor que sabe que es no en el generador.

Editar : Sí, esto saltará 1 elemento en el generador. A menudo, sin embargo, verifico si un generador está vacío solo para fines de validación, luego realmente no lo uso. O de lo contrario hago algo como:

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

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

Es decir, esto funciona si su generador proviene de una función , como en generator().


44
¿Por qué esta no es la mejor respuesta? En caso de que el generador regrese None?
Sait

8
Probablemente porque esto te obliga a consumir el generador en lugar de solo probar si está vacío.
bfontaine

3
Es malo porque en el momento en que llama al siguiente (generador, Ninguno) omitirá 1 elemento si está disponible
Nathan Do

Correcto, vas a perder el primer elemento de tu gen y también vas a consumir tu gen en lugar de probar si está vacío.
AJ

12

El mejor enfoque, en mi humilde opinión, sería evitar una prueba especial. La mayoría de las veces, el uso de un generador es la prueba:

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)

Si eso no es lo suficientemente bueno, aún puede realizar una prueba explícita. En este punto, thingcontendrá el último valor generado. Si no se generó nada, será indefinido, a menos que ya haya definido la variable. Puede verificar el valor de thing, pero eso es poco confiable. En cambio, solo establezca una bandera dentro del bloque y verifíquela después:

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

3
Esta solución intentará consumir todo el generador, por lo que será inutilizable para generadores infinitos.
Viktor Stískala

@ ViktorStískala: No entiendo tu punto. Sería tonto probar si un generador infinito produce algún resultado.
vezult

Quería señalar que su solución podría contener una interrupción en el ciclo for, porque no está procesando los otros resultados y es inútil que se generen. range(10000000)es un generador finito (Python 3), pero no necesita revisar todos los elementos para saber si genera algo.
Viktor Stískala

1
@ ViktorStískala: Entendido. Sin embargo, mi punto es este: en general, realmente desea operar en la salida del generador. En mi ejemplo, si no se genera nada, ahora lo sabes. De lo contrario, usted opera en la salida generada según lo previsto: "El uso del generador es la prueba". No es necesario realizar pruebas especiales o consumir sin sentido la salida del generador. He editado mi respuesta para aclarar esto.
vezult

8

Odio ofrecer una segunda solución, especialmente una que no usaría yo mismo, pero, si absolutamente tuviera que hacer esto y no consumir el generador, como en otras respuestas:

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)

Ahora realmente no me gusta esta solución, porque creo que no es así como se deben usar los generadores.


4

Me doy cuenta de que esta publicación tiene 5 años en este momento, pero la encontré mientras buscaba una forma idiomática de hacer esto, y no vi mi solución publicada. Entonces para la posteridad:

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)

Por supuesto, como estoy seguro de que muchos comentaristas señalarán, esto es hacky y solo funciona en ciertas situaciones limitadas (donde los generadores están libres de efectos secundarios, por ejemplo). YMMV.


1
Esto solo llamará al gengenerador una vez por cada elemento, por lo que los efectos secundarios no son un problema tan grave. Pero almacenará una copia de todo lo que se ha extraído del generador a través de b, pero no a través de a, por lo que las implicaciones de la memoria son similares a simplemente ejecutar list(gen)y verificar eso.
Matthias Fripp

Tiene dos problemas. 1. Esta herramienta iterativa puede requerir un importante almacenamiento auxiliar (dependiendo de la cantidad de datos temporales que deban almacenarse). En general, si un iterador usa la mayoría o la totalidad de los datos antes de que comience otro iterador, es más rápido usar list () en lugar de tee (). 2. Los iteradores de tee no son seguros para subprocesos. Se puede generar un RuntimeError cuando se utilizan iteradores simultáneos devueltos por la misma llamada tee (), incluso si el iterable original es seguro para subprocesos.
AJ

3

Perdón por el enfoque obvio, pero la mejor manera sería hacer:

for item in my_generator:
     print item

Ahora ha detectado que el generador está vacío mientras lo está utilizando. Por supuesto, el elemento nunca se mostrará si el generador está vacío.

Es posible que esto no coincida exactamente con su código, pero para eso está el modismo del generador: iterar, por lo que tal vez podría cambiar su enfoque ligeramente o no usar generadores en absoluto.


O ... el interrogador podría proporcionar alguna pista de por qué uno trataría de detectar un generador vacío.
S.Lott

¿quiso decir "no se mostrará nada porque el generador está vacío"?
SilentGhost

S.Lott. Estoy de acuerdo. No puedo ver por qué. Pero creo que incluso si hubiera una razón, el problema podría ser mejor para usar cada elemento en su lugar.
Ali Afshar

1
Esto no le dice al programa si el generador estaba vacío.
Ethan Furman el

3

Todo lo que necesita hacer para ver si un generador está vacío es intentar obtener el siguiente resultado. Por supuesto, si no está listo para usar ese resultado, debe almacenarlo para devolverlo más tarde.

Aquí hay una clase de contenedor que se puede agregar a un iterador existente para agregar una __nonzero__prueba, para que pueda ver si el generador está vacío con un simple if. Probablemente también se puede convertir en un decorador.

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)

Así es como lo usarías:

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

Tenga en cuenta que puede verificar el vacío en cualquier momento, no solo al comienzo de la iteración.


Esto se dirige en la dirección correcta. Debe modificarse para permitir mirar hacia adelante tanto como lo desee, almacenando tantos resultados como sea necesario. Idealmente, permitiría empujar elementos arbitrarios en la cabeza de la secuencia. Un iterador presionable es una abstracción muy útil que uso a menudo.
sfkleach

@sfkleach No veo la necesidad de complicar esto para múltiples adelantos, es bastante útil como es y responde la pregunta. A pesar de que esta es una vieja pregunta, todavía tiene un aspecto ocasional, por lo que si desea dejar su propia respuesta, alguien podría encontrarla útil.
Mark Ransom

Mark tiene razón en que su solución responde a la pregunta, que es el punto clave. Debería haberlo redactado mejor. Lo que quise decir es que los iteradores empujables con retroceso ilimitado son un idioma que he encontrado extremadamente útil y la implementación es posiblemente aún más simple. Como se sugiere, publicaré el código de variante.
sfkleach

2

Impulsado por Mark Ransom, aquí hay una clase que puede usar para ajustar cualquier iterador para que pueda mirar hacia adelante, empujar los valores nuevamente en la secuencia y verificar si está vacío. Es una idea simple con una implementación simple que he encontrado muy útil en el pasado.

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

Simplemente caí en este hilo y me di cuenta de que faltaba una respuesta muy simple y fácil de leer:

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

Si se supone que no debemos consumir ningún artículo, entonces debemos reinyectar el primer artículo en el generador:

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

Ejemplo:

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

Al final del generador StopIterationse genera, ya que en su caso se alcanza el final de inmediato, se genera una excepción. Pero normalmente no debe verificar la existencia del siguiente valor.

Otra cosa que puedes hacer es:

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

2
Lo que realmente consume todo el generador. Lamentablemente, de la pregunta no está claro si este es un comportamiento deseable o indeseable.
S.Lott

como cualquier otra forma de "tocar" el generador, supongo.
SilentGhost

Sé que esto es viejo, pero el uso de 'lista ()' no puede ser la mejor manera, si la lista generada no está vacío pero de hecho grande, entonces esto es un desperdicio innecesario
Chris_Rands

1

Si necesita saber antes de usar el generador, entonces no, no hay una manera simple. Si puede esperar hasta después de haber usado el generador, hay una manera simple:

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

Simplemente envuelva el generador con itertools.chain , coloque algo que represente el final del iterable como el segundo iterable, luego simplemente verifique eso.

Ex:

import itertools

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

Ahora todo lo que queda es verificar el valor que agregamos al final del iterable, cuando lo lea, eso significará el final

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

Use en eog = object()lugar de asumir que float('-inf')eso nunca ocurrirá en el iterable.
bfontaine

@bfontaine Buena idea
smac89

1

En mi caso lo que necesitaba saber si una gran cantidad de generadores fue poblada antes de que pasé a una función, que se fusionó los elementos, es decir, zip(...). La solución es similar, pero lo suficientemente diferente, de la respuesta aceptada:

Definición:

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"

Mi problema particular tiene la propiedad de que los iterables están vacíos o tienen exactamente el mismo número de entradas.


1

Encontré solo esta solución que también funciona para iteraciones vacías.

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 si no desea usar una excepción para esto, intente usar

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)

En la solución marcada no es posible usarlo para generadores vacíos como

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Aquí está mi enfoque simple que uso para seguir devolviendo un iterador mientras verifico si se produjo algo, solo verifico si el ciclo se ejecuta:

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

0

Aquí hay un decorador simple que envuelve el generador, por lo que devuelve None si está vacío. Esto puede ser útil si su código necesita saber si el generador producirá algo antes de recorrerlo.

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 ejemplo donde esto es útil es en el código de plantillas, es decir, jinja2

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

Esto llama a la función del generador dos veces, por lo que incurrirá en el costo de arranque del generador dos veces. Eso podría ser sustancial si, por ejemplo, la función de generador es una consulta de base de datos.
Ian Goldby

0

usando islice solo necesita verificar hasta la primera iteración para descubrir si está vacío.

de itertools import islice

def isempty (iterable):
    lista de retorno (islice (iterable, 1)) == []


Lo sentimos, esta es una lectura de consumo ... Tengo que hacer el try / catch con StopIteration
Quin

0

¿Qué pasa con el uso de any ()? Lo uso con generadores y funciona bien. Aquí hay un chico explicando un poco sobre esto


2
No podemos usar "any ()" para todo el generador. Solo traté de usarlo con un generador que contiene múltiples marcos de datos. Recibí este mensaje "El valor de verdad de un DataFrame es ambiguo". en cualquiera (my_generator_of_df)
probitaille

any(generator)funciona cuando sabe que el generador generará valores a los que se puede convertir bool: los tipos de datos básicos (por ejemplo, int, string) funcionan. any(generator)será False cuando el generador esté vacío, o cuando el generador solo tenga valores falsos; por ejemplo, si un generador va a generar 0 '' (cadena vacía) y False, seguirá siendo False. Este podría o no ser el comportamiento previsto, siempre que lo sepas :)
Daniel

0

Use la función de vistazo en 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

El iterador devuelto por esta función será equivalente al original pasado como argumento.


-2

Lo resolví usando la función de suma. Vea a continuación un ejemplo que utilicé con glob.iglob (que devuelve un generador).

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

* Esto probablemente no funcionará para generadores ENORMES, pero debería funcionar bien para listas más pequeñas

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.