Como sei se um gerador está vazio desde o início?


146

Existe uma maneira simples de testar se o gerador não tem itens, como peek, hasNext, isEmpty, algo nesse sentido?


Corrija-me se eu estiver errado, mas se você pudesse criar uma solução verdadeiramente genérica para qualquer gerador, seria o equivalente a definir pontos de interrupção nas declarações de rendimento e ter a capacidade de "retroceder". Isso significaria clonar o quadro da pilha sobre rendimentos e restaurá-los no StopIteration?

Bem, acho que restaure-os StopIteration ou não, mas pelo menos o StopIteration diria que estava vazio. Sim, eu preciso dormir ... #

4
Eu acho que sei por que ele quer isso. Se você está desenvolvendo web com modelos e passando o valor de retorno para um modelo como Cheetah ou algo assim, a lista vazia []é convenientemente Falsey, então você pode fazer uma verificação se e fazer um comportamento especial para algo ou nada. Os geradores são verdadeiros mesmo que não produzam elementos.
jpsimons

Aqui está o meu caso de uso ... Estou usando glob.iglob("filepattern")um padrão curinga fornecido pelo usuário e quero avisar o usuário se o padrão não corresponder a nenhum arquivo. Claro que posso solucionar isso de várias maneiras, mas é útil poder testar com clareza se o iterador ficou vazio ou não.
precisa saber é

Pode ser usada esta solução: stackoverflow.com/a/11467686/463758
balki

Respostas:


53

A resposta simples para sua pergunta: não, não há uma maneira simples. Existem muitas soluções alternativas.

Realmente não deveria haver uma maneira simples, por causa do que são os geradores: uma maneira de gerar uma sequência de valores sem manter a sequência na memória . Portanto, não há travessia para trás.

Você pode escrever uma função has_next ou talvez até colocá-la em um gerador como um método com um decorador sofisticado, se você quiser.


2
justo o suficiente, isso faz sentido. eu sabia que não havia como encontrar o comprimento de um gerador, mas pensei que poderia ter perdido uma maneira de descobrir se ele inicialmente geraria alguma coisa.
Dan

1
Ah, e para referência, tentei implementar minha própria sugestão de "decorador chique". DIFÍCIL. Aparentemente, copy.deepcopy não funciona em geradores.
David Berger

47
Não sei se posso concordar com "não deve haver uma maneira simples". Existem muitas abstrações na ciência da computação projetadas para produzir uma sequência de valores sem reter a sequência na memória, mas que permitem ao programador perguntar se existe outro valor sem removê-lo da "fila", se houver. Existe uma única espiada à frente sem a necessidade de "retroceder". Isso não quer dizer que um design de iterador deve fornecer esse recurso, mas com certeza é útil. Talvez você esteja se opondo com base no fato de que o primeiro valor pode mudar após a espiada?
Larsh

9
Estou contestando o fato de que uma implementação típica nem sequer calcula um valor até que seja necessário. Pode-se forçar a interface a fazer isso, mas isso pode ser sub-ideal para implementações leves.
David Berger

6
@ S.Lott, você não precisa gerar a sequência inteira para saber se a sequência está vazia ou não. O valor de um elemento de armazenamento é suficiente - veja minha resposta.
Mark Ransom

98

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.

2
Não entendo muito bem o retorno do primeiro elemento duas vezes return first, itertools.chain([first], rest).
Njzk2

6
@ njzk2 Eu estava indo para uma operação de "espiada" (daí o nome da função). wiki "peek é uma operação que retorna o valor da parte superior da coleção sem remover o valor dos dados"
John Fouhy

Isso não funcionará se o gerador for projetado para gerar Nenhum. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

4
@Paul Veja atentamente os valores retornados. Se o gerador estiver pronto - ou seja, não retornando None, mas aumentando StopIteration- o resultado da função é None. Caso contrário, é uma tupla, o que não é None.
Fund Monica's Lawsuit

Isso me ajudou muito no meu projeto atual. Encontrei um exemplo semelhante no código do módulo de biblioteca padrão do python 'mailbox.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
par par

29

Uma maneira simples é usar o parâmetro opcional for next () que é usado se o gerador estiver esgotado (ou vazio). Por exemplo:

iterable = some_generator()

_exhausted = object()

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

Edit: Corrigido o problema apontado no comentário de mehtunguh.


1
Não. Isso está incorreto para qualquer gerador em que o primeiro valor gerado seja falso.
precisa saber é o seguinte

7
Use um em object()vez de classpara torná-lo uma linha mais curta _exhausted = object():; if next(iterable, _exhausted) is _exhausted:
Messa 21/03

13

next(generator, None) is not None

Ou substitua, Nonemas o valor que você sabe que não está no seu gerador.

Edit : Sim, isso irá pular 1 item no gerador. Freqüentemente, porém, verifico se um gerador está vazio apenas para fins de validação e, na verdade, não o uso. Ou então eu faço algo como:

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

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

Ou seja, isso funciona se o seu gerador vier de uma função , como em generator().


4
Por que essa não é a melhor resposta? Caso o gerador retorne None?
Sait

8
Provavelmente porque isso força você a realmente consumir o gerador em vez de apenas testar se está vazio.
precisa saber é

3
É ruim porque no momento em que você chamar seguinte (gerador, None) você vai saltar 1 ponto, se estiver disponível
Nathan Do

Correto, você perderá o primeiro elemento do seu gen e também consumirá seu gen em vez de testar se estiver vazio.
AJ

12

A melhor abordagem, IMHO, seria evitar um teste especial. Na maioria das vezes, o uso de um gerador é 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 for bom o suficiente, você ainda poderá executar um teste explícito. Neste ponto, thingconterá o último valor gerado. Se nada foi gerado, será indefinido - a menos que você já tenha definido a variável. Você pode verificar o valor de thing, mas isso é um pouco confiável. Em vez disso, basta definir um sinalizador dentro do bloco e verificar depois:

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

3
Esta solução tentará consumir todo o gerador, tornando-o inutilizável para geradores infinitos.
Viktor Stískala

@ ViktorStískala: Eu não entendo o seu ponto. Seria tolo testar se um gerador infinito produzisse algum resultado.
vezult 07/11/13

Queria ressaltar que sua solução pode conter quebra no loop for, porque você não está processando os outros resultados e é inútil gerá-los. range(10000000)é um gerador finito (Python 3), mas você não precisa passar por todos os itens para descobrir se ele gera alguma coisa.
Viktor Stískala

1
@ ViktorStískala: Entendido. No entanto, meu argumento é o seguinte: Geralmente, você realmente deseja operar na saída do gerador. No meu exemplo, se nada for gerado, você já o conhece. Caso contrário, você opera na saída gerada conforme o esperado - "O uso do gerador é o teste". Não há necessidade de testes especiais ou consumo desnecessário da saída do gerador. Eu editei minha resposta para esclarecer isso.
vezult 12/11/13

8

Detesto oferecer uma segunda solução, especialmente uma que eu não usaria, mas, se você tivesse que fazer isso absolutamente 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 desta solução, porque acredito que não é assim que os geradores devem ser usados.


4

Percebo que este post tem 5 anos neste momento, mas o encontrei enquanto procurava uma maneira idiomática de fazer isso e não vi 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)

Obviamente, como tenho certeza de que muitos comentaristas apontarão, isso é hacky e só funciona em determinadas situações limitadas (onde os geradores são livres de efeitos colaterais, por exemplo). YMMV.


1
Isso chamará o gengerador apenas uma vez para cada item, portanto os efeitos colaterais não são um problema muito ruim. Mas ele armazenará uma cópia de tudo o que foi extraído do gerador via b, mas não via a, portanto as implicações de memória são semelhantes a apenas executar list(gen)e verificar isso.
Matthias Fripp

Tem dois problemas. 1. Essa ferramenta pode exigir armazenamento auxiliar significativo (dependendo da quantidade de dados temporários que precisam ser armazenados). Em geral, se um iterador usa a maioria ou todos os dados antes de outro iterador iniciar, é mais rápido usar list () em vez de tee (). 2. Os iteradores tee não são seguros para threads. Um RuntimeError pode ser gerado ao usar iteradores simultaneamente retornados pela mesma chamada tee (), mesmo que o iterável original seja seguro para threads.
AJ

3

Desculpe a abordagem óbvia, mas a melhor maneira seria:

for item in my_generator:
     print item

Agora você detectou que o gerador está vazio enquanto você o está usando. Obviamente, o item nunca será exibido se o gerador estiver vazio.

Isso pode não se encaixar exatamente no seu código, mas é para isso que serve o idioma: iterar, portanto, talvez você possa mudar um pouco sua abordagem ou não usar geradores.


Ou ... o questionador poderia fornecer alguma dica sobre por que alguém tentaria detectar um gerador vazio?
315/09 S.Lott

você quis dizer "nada será exibido porque o gerador está vazio"?
23410 SilentGhost

S.Lott. Concordo. Não vejo o porquê. Mas acho que, mesmo que houvesse um motivo, seria melhor mudar o problema para usar cada item.
27630 Ali Afshar

1
Isso não informa ao programa se o gerador estava vazio.
Ethan Furman

3

Tudo o que você precisa fazer para verificar se um gerador está vazio é tentar obter o próximo resultado. Obviamente, se você não estiver pronto para usar esse resultado, precisará armazená-lo para devolvê-lo mais tarde.

Aqui está uma classe de wrapper que pode ser adicionada a um iterador existente para adicionar um __nonzero__teste, para que você possa ver se o gerador está vazio com um simples if. Provavelmente também pode 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)

Veja como você o usaria:

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

Observe que você pode verificar o vazio a qualquer momento, não apenas no início da iteração.


Isso está indo na direção certa. Ele deve ser modificado para permitir espreitar o máximo que desejar, armazenando quantos resultados forem necessários. Idealmente, isso permitiria empurrar itens arbitrários na cabeça do fluxo. Um iterador empurrável é uma abstração muito útil que costumo usar.
Sfkleach

@sfkleach Não vejo a necessidade de complicar isso por vários olhares antecipados, é bastante útil e responde à pergunta. Mesmo que essa seja uma pergunta antiga, ainda está recebendo uma aparência ocasional; portanto, se você quiser deixar sua própria resposta, alguém poderá achar útil.
Mark Ransom

Mark está certo ao saber que sua solução responde à pergunta, que é o ponto principal. Eu deveria ter formulado melhor. O que eu quis dizer foi que iteradores empurráveis ​​com resposta ilimitada são um idioma que eu achei extremamente útil e a implementação é sem dúvida ainda mais simples. Conforme sugerido, publicarei o código da variante.
sfkleach 7/01

2

Solicitado por Mark Ransom, aqui está uma classe que você pode usar para agrupar qualquer iterador, para que você possa espreitar adiante, enviar valores de volta ao fluxo e verificar se estão vazios. É uma ideia simples, com uma implementação simples, que achei 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)

2

Apenas caí nessa discussão e percebi que faltava uma resposta muito simples e fácil de ler:

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

Se não devemos consumir nenhum item, precisamos injetar novamente 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]

1
>>> 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 gerador StopIterationé gerado, pois no seu caso o fim é alcançado imediatamente, a exceção é gerada.Mas normalmente você não deve verificar a existência do próximo valor.

Outra coisa que você pode fazer é:

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

2
O que realmente consome todo o gerador. Infelizmente, não está claro com a pergunta se esse é um comportamento desejável ou indesejável.
315/09 S.Lott

como qualquer outra maneira de "tocar" o gerador, suponho.
SilentGhost 19/03/2009

Sei que este é antiga, mas usando 'list ()' não pode ser a melhor maneira, se a lista gerada não está vazio, mas na verdade grande, então este é desnecessariamente desperdício
Chris_Rands

1

Se você precisar saber antes de usar o gerador, não, não existe uma maneira simples. Se você pode esperar até depois de usar o gerador, existe 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()

1

Simplesmente envolva o gerador com itertools.chain , coloque algo que represente o final do iterável como o segundo iterável e, em seguida, simplesmente verifique isso.

Ex:

import itertools

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

Agora tudo o que resta é verificar o valor que acrescentamos ao final do iterável, quando você o ler, isso significará o final

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

Use em eog = object()vez de assumir que float('-inf')nunca ocorrerá no iterável.
precisa saber é

@bfontaine Boa idéia
smac89

1

No meu caso, eu precisava saber se uma série de geradores foi preenchida antes de passar para uma função que mesclava os itens, ou seja zip(...),. A solução é semelhante, mas diferente o suficiente, 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"

Meu problema específico tem a propriedade de que as iteráveis ​​estão vazias ou têm exatamente o mesmo número de entradas.


1

Eu encontrei apenas essa solução trabalhando para iterações vazias também.

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)

Ou, se você não quiser usar a exceção para esta tentativa, use

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)

Na solução marcada, não é possível usá-lo para geradores vazios, como

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Aqui está minha abordagem simples que eu uso para continuar retornando um iterador enquanto verifica se algo foi produzido, apenas verifico se o loop é executado:

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

0

Aqui está um decorador simples que envolve o gerador, e ele retorna Nenhum se estiver vazio. Isso pode ser útil se o seu código precisar saber se o gerador produzirá alguma coisa antes de passar por ele.

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 em que isso é útil é no código de modelo - ie jinja2

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

Isso chama a função do gerador duas vezes, portanto incorrerá no custo de inicialização do gerador duas vezes. Isso pode ser substancial se, por exemplo, a função do gerador for uma consulta ao banco de dados.
Ian Goldby

0

usando o islice, você só precisa verificar a primeira iteração para descobrir se está vazia.

de itertools import islice

def isempty (iterável):
    lista de retorno (islice (iterável, 1)) == []


Desculpe, esta é uma leitura de consumo ... tem que fazer o try / catch com StopIteration
Quin

0

Que tal usar any ()? Eu uso com geradores e está funcionando bem. Aqui tem um cara explicando um pouco sobre isso


2
Não podemos usar "any ()" para tudo gerador. Apenas tentei usá-lo com um gerador que contém vários quadros de dados. Recebi esta mensagem "O valor verdadeiro de um DataFrame é ambíguo". em qualquer (my_generator_of_df)
probitaille 8/11

any(generator)funciona quando você sabe que o gerador irá gerar valores que podem ser convertidos para bool- os tipos de dados básicos (por exemplo, int, string) funcionam. any(generator)será Falso quando o gerador estiver vazio, ou quando o gerador tiver apenas valores falsos - por exemplo, se um gerador gerar 0 '' (string vazia) e Falso, ele ainda será Falso. Isso pode ou não ser o comportamento pretendido, contanto que você está ciente de que :)
Daniel

0

Use a função de espiar no 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 esta função será equivalente ao original transmitido como argumento.


-2

Eu o resolvi usando a função soma. Veja abaixo um exemplo que usei com glob.iglob (que retorna um gerador).

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

* Isso provavelmente não funcionará para geradores ENORME, mas deve funcionar bem para listas menores

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.