python - with - ¿Cómo puedo saber si un generador está vacío desde el principio?
what are python generators used for (19)
¿Qué hay de usar any ()? Lo uso con generadores y funciona bien. Here hay un hombre explicando un poco sobre esto
¿Hay una manera simple de probar si el generador no tiene elementos, como peek, hasNext, isEmpty, algo parecido?
Aquí está mi enfoque simple que utilizo para seguir devolviendo un iterador mientras verifico si algo fue cedido. Solo compruebo 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 pasar por él.
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 %}
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, no estará definido, a menos que ya haya definido la variable. Podrías comprobar el valor de la thing
, pero eso es un poco poco confiable. En su lugar, simplemente establece un indicador dentro del bloque y luego verifícalo:
if not thing_generated:
print "Avast, ye scurvy dog!"
En mi caso, necesitaba saber si una gran cantidad de generadores estaban ocupados antes de pasarlo a una función, que fusionaba 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.
La respuesta simple a su pregunta: no, no hay una manera simple. Hay muchas soluciones alternativas.
Realmente no debería ser una forma sencilla, por lo que son los generadores: una forma de generar una secuencia de valores sin mantener la secuencia en la memoria . Entonces no hay atraso hacia atrás.
Podrías escribir una función has_next o quizás incluso ponerla en un generador como método con un decorador elegante si quisieras.
Lo resolví usando la función 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 muy bien para listas más pequeñas
Me doy cuenta de que esta publicación tiene 5 años en este momento, pero la encontré al buscar una forma idiomática de hacerlo, 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 que muchos comentaristas señalarán, esto es raro y solo funciona en ciertas situaciones limitadas (donde los generadores son libres de efectos secundarios, por ejemplo). YMMV.
Odio ofrecer una segunda solución, especialmente una que no usaría yo mismo, pero, si 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 usarán los generadores.
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 artículo nunca se mostrará si el generador está vacío.
Puede que esto no encaje exactamente con su código, pero para eso es la expresión idiomática del generador: iterar, por lo que tal vez pueda cambiar ligeramente su enfoque o no usar generadores en absoluto.
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 de la iterable, cuando lo lees, 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
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.
Todo lo que necesita hacer para ver si el generador está vacío es intentar obtener el siguiente resultado. Por supuesto, si no estás listo para usar ese resultado, entonces debes almacenarlo para devolverlo más tarde.
Aquí hay una clase contenedora que se puede agregar a un iterador existente para agregar una prueba __nonzero__
, 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 = self.source.next()
self.stored = True
except StopIteration:
return False
return True
def next(self):
if self.stored:
self.stored = False
return self.value
return self.source.next()
Así es como lo usarías:
with open(filename, ''r'') as f:
f = GenWrapper(f)
if f:
print ''Not empty''
else:
print ''Empty''
Una forma 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.
Use la función peek 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 entregado como argumento.
next(generator, None) is not None
O reemplace None
pero sea cual sea el valor que sepa, no está en su generador.
Editar : Sí, esto omitirá 1 elemento en el generador. A menudo, sin embargo, verifico si un generador está vacío solo con fines de validación, entonces 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 el generator()
.
con islice solo necesita comprobar hasta la primera iteración para descubrir si está vacía.
de itertools import islice
def isempty (iterable):
lista de devolución (islice (iterable, 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, StopIteration
, ya que en su caso el fin se alcanza inmediatamente, se StopIteration
una excepción. Pero normalmente no debes verificar la existencia del siguiente valor.
Otra cosa que puedes hacer es:
>>> gen = (i for i in [])
>>> if not list(gen):
print(''empty generator'')