¿Hay una manera sencilla de comprobar si el generador no tiene elementos, como peek
, hasNext
, isEmpty
, algo por el estilo?
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.
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.
return first, itertools.chain([first], rest)
.
def gen(): for pony in range(4): yield None if pony == 2 else pony
None
, sino que sube StopIteration
, el resultado de la función es None
. De lo contrario, es una tupla, que no lo es None
.
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
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.
object()
lugar de class
para hacerlo una línea más corta _exhausted = object()
:; if next(iterable, _exhausted) is _exhausted:
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()
.
None
?
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, thing
contendrá 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!"
range(10000000)
es un generador finito (Python 3), pero no necesita revisar todos los elementos para saber si genera algo.
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.
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.
gen
generador 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.
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.
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.
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)
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]
>>> 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 StopIteration
se 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')
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()
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
eog = object()
lugar de asumir que float('-inf')
eso nunca ocurrirá en el iterable.
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.
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()
Esta es una pregunta vieja y respondida, pero como nadie la ha mostrado antes, aquí va:
for _ in generator:
break
else:
print('Empty')
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
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 %}
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)) == []
¿Qué pasa con el uso de any ()? Lo uso con generadores y funciona bien. Aquí hay un chico explicando un poco sobre esto
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 :)
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.
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
[]
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.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.