Como posso saber se um gerador está vazio desde o início?
Pergunta
Existe uma maneira simples de testar se o gerador não tem itens, como espiada, hasNext, isEmpty, algo nesse sentido?
Solução
A resposta simples para a pergunta: não, não há nenhuma maneira simples. Há toda uma série de soluções alternativas.
Há realmente não deve ser uma maneira simples, por causa do que geradores são: uma forma de saída de uma sequência de valores sem segurar a seqüência na memória . Portanto, não há passagem de trás.
Você poderia escrever uma função has_next ou talvez até mesmo um tapa-lo para um gerador como um método com um decorador de fantasia, se você quisesse.
Outras dicas
Sugestão:
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.
Uma maneira simples é usar o parâmetro opcional para next () que é utilizado, se o gerador estiver exausta (ou esvaziar). Por exemplo:
iterable = some_generator()
_exhausted = object()
if next(iterable, _exhausted) == _exhausted:
print('generator is empty')
Editar:. Corrigido o problema apontado no comentário de mehtunguh
A melhor abordagem, IMHO, seria para evitar um teste especial. Na maioria das vezes, o uso de um gerador de é o teste:
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 isso não é bom o suficiente, você ainda pode realizar um teste explícito. Neste ponto, thing
conterá o último valor gerado. Se nada foi gerado, ele será indefinido - a menos que você já tenha definido a variável. Você pode verificar o valor de thing
, mas isso é um pouco confiáveis. Em vez disso, basta definir uma bandeira dentro do bloco e verificá-lo depois:
if not thing_generated:
print "Avast, ye scurvy dog!"
next(generator, None) is not None
Ou substitua None
mas o valor que você sabe que é não em seu gerador.
Editar : Sim, isso vai saltar 1 item no gerador. Muitas vezes, porém, eu verificar se um gerador está vazio apenas para fins de validação, então realmente não usá-lo. Ou caso contrário eu fazer algo como:
def foo(self):
if next(self.my_generator(), None) is None:
raise Exception("Not initiated")
for x in self.my_generator():
...
Isso é, isso funciona se o seu gerador vem de um Função , como em generator()
.
Eu odeio para oferecer uma segunda solução, especialmente um que eu não usaria mim mesmo, mas, se você absolutamente estiveste para fazer isso e não consumir o gerador, como em outras respostas:
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)
Agora eu realmente não gosto dessa solução, porque acredito que isto não é como geradores estão a ser utilizado.
Desculpem a abordagem óbvia, mas a melhor maneira seria fazer:
for item in my_generator:
print item
Agora você detectou que o gerador está vazio, enquanto você estiver usando-o. Claro, o item não será exibido se o gerador está vazio.
Este pode não se encaixar exatamente com seu código, mas isso é o que o idioma do gerador é a seguinte:. Iteração, então talvez você pode mudar a sua abordagem um pouco, ou não usar geradores de todo
Eu percebo que este post é de 5 anos, neste ponto, mas eu achei que, enquanto procura uma maneira idiomática de fazer isso, e não viu a minha solução postada. Assim, para a posteridade:
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)
Claro que, como eu tenho certeza que muitos comentaristas irá apontar, este é hacky e só funciona em tudo em certas situações limitadas (onde os geradores são livres de efeitos colaterais, por exemplo). YMMV.
Tudo que você precisa fazer para ver se um gerador está vazia é tentar obter o próximo resultado. Claro, se você não estiver pronto usar esse resultado, então você tem de armazená-lo para devolvê-lo novamente mais tarde.
Aqui está uma classe de invólucro que pode ser adicionado a uma iteração existente para adicionar um teste __nonzero__
, assim você pode ver se o gerador está vazio com um if
simples. Provavelmente, pode também ser transformado em um 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)
Aqui está como você usá-lo:
with open(filename, 'r') as f:
f = GenWrapper(f)
if f:
print 'Not empty'
else:
print 'Empty'
Note que você pode verificar se há vazio a qualquer momento, não apenas no início da iteração.
>>> gen = (i for i in [])
>>> next(gen)
Traceback (most recent call last):
File "<pyshell#43>", line 1, in <module>
next(gen)
StopIteration
No final do StopIteration
gerador é elevado, uma vez que na sua extremidade caso é alcançado imediatamente, exceção é gerada. Mas normalmente você não deve verificar se há existência de valor seguinte.
Outra coisa que você pode fazer é:
>>> gen = (i for i in [])
>>> if not list(gen):
print('empty generator')
No meu caso eu precisava saber se uma série de geradores foi povoada antes de eu passou para uma função, que se fundiu os itens, ou seja, zip(...)
. A solução é semelhante, mas o suficiente diferente, a partir da resposta aceita:
Definição:
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"
O meu problema particular, tem a propriedade de que os iterables está vazio ou tem exatamente o mesmo número de entradas.
Apenas caiu sobre esta discussão e percebeu que uma forma muito simples e fácil de ler a resposta estava faltando:
def is_empty(generator):
for item in generator:
return False
return True
Se não estamos supor para consumir qualquer item então precisamos re-injetar o primeiro item no gerador:
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
Exemplo:
>>> 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]
Se você precisa saber antes você usar o gerador, então não, não há nenhuma maneira simples. Se você pode esperar até que após você usou o gerador, há uma maneira simples:
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()
Aqui está a minha abordagem simples que eu uso para manter em retornar um iterador ao verificar se algo foi cedido Eu apenas verificar se a execução do loop:
n = 0
for key, value in iterator:
n+=1
yield key, value
if n == 0:
print ("nothing found in iterator)
break
Aqui está um decorador simples que envolve o gerador, então ele retorna None se vazio. Isso pode ser útil se as suas necessidades de código para saber se o gerador irá produzir qualquer coisa antes looping através dela.
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')
Um exemplo onde isso é útil é em código templates - ou seja Jinja2
{% if content_generator %}
<section>
<h4>Section title</h4>
{% for item in content_generator %}
{{ item }}
{% endfor %
</section>
{% endif %}
Basta enrolar o gerador com itertools.chain , put algo que irá representar o final do iterable como o segundo iterable, em seguida, basta verificar para isso.
Ex:
import itertools
g = some_iterable
eog = object()
wrap_g = itertools.chain(g, [eog])
Agora tudo o que resta é para verificar se esse valor nós acrescentado ao final do iterable, quando você lê-lo, em seguida, que irá significar o fim
for value in wrap_g:
if value == eog: # DING DING! We just found the last element of the iterable
pass # Do something
usando islice você só precisa check-up para a primeira iteração para descobrir se ele está vazio.
a partir itertools importação islice
def isempty (iterable):
Lista de retorno (islice (iterable, 1)) == []
Que tal usar qualquer ()? Eu usá-lo com geradores e está funcionando bem. Aqui não é cara explicando um pouco sobre este
Use a função espiada em 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
O iterador retornado por essa função será equivalente ao original passado como um argumento.
Alertado por Mark Ransom, aqui está uma classe que você pode usar para embrulhar qualquer iterador de modo que você pode espreitar à frente, os valores de empurrar para trás para o fluxo e verificar se há vazio. É uma idéia simples, com uma implementação simples que eu encontrei muito útil no passado.
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)
Eu resolvi-lo usando a função soma. Ver abaixo para um exemplo I utilizado com glob.iglob (que retorna um gerador).
def isEmpty():
files = glob.iglob(search)
if sum(1 for _ in files):
return True
return False
* Este será, provavelmente, não trabalho para geradores enorme, mas deve executar bem para listas menores