web-dev-qa-db-fra.com

Comment savoir si un générateur est vide dès le début?

Existe-t-il un moyen simple de tester si le générateur n'a pas d'éléments, comme peek, hasNext, isEmpty, etc.

102
Dan

La réponse simple à votre question: non, il n'y a pas de solution simple. Il y a beaucoup de solutions de rechange.

Il ne devrait pas y avoir de moyen simple, à cause de ce que sont les générateurs: un moyen de sortir une séquence de valeurs sans conserver la séquence en mémoire . Donc, il n'y a pas de traversée en arrière.

Vous pouvez écrire une fonction has_next ou même l'adapter à un générateur en tant que méthode avec un décorateur de fantaisie si vous le souhaitez.

44
David Berger

Suggestion:

def peek(iterable):
    try:
        first = next(iterable)
    except StopIteration:
        return None
    return first, itertools.chain([first], iterable)

Usage:

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.
73
John Fouhy

Un moyen simple consiste à utiliser le paramètre facultatif de next () qui est utilisé si le générateur est épuisé (ou vide). Par exemple: 

iterable = some_generator()

_exhausted = object()

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

Edit: Correction du problème signalé dans le commentaire de mehtunguh.

22
razz0

La meilleure approche, à mon humble avis, serait d’éviter un test spécial. La plupart du temps, l’utilisation d’un générateur est le test:

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 cela ne suffit pas, vous pouvez toujours effectuer un test explicite. À ce stade, thing contiendra la dernière valeur générée. Si rien n'a été généré, ce sera indéfini - à moins que vous n'ayez déjà défini la variable. Vous pouvez vérifier la valeur de thing, mais c'est un peu peu fiable. Au lieu de cela, définissez simplement un drapeau dans le bloc et vérifiez-le ensuite:

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

Je déteste proposer une deuxième solution, en particulier une solution que je ne voudrais pas utiliser moi-même, mais, si vous deviez absolument le faire et ne pas utiliser le générateur, comme dans d'autres réponses

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)

Maintenant, je n'aime vraiment pas cette solution, parce que je crois que ce n'est pas ainsi que les générateurs doivent être utilisés.

8
Ali Afshar

Désolé pour l'approche évidente, mais le meilleur moyen serait de faire:

for item in my_generator:
     print item

Vous avez maintenant détecté que le générateur est vide pendant son utilisation. Bien sûr, l'élément ne sera jamais affiché si le générateur est vide.

Cela peut ne pas correspondre parfaitement à votre code, mais c'est à cela que sert l'idiome du générateur: itérer, alors vous pourriez peut-être modifier légèrement votre approche ou ne pas utiliser de générateurs.

4
Ali Afshar

Tout ce que vous avez à faire pour voir si un générateur est vide est d’essayer d’obtenir le résultat suivant. Bien sûr, si vous n'êtes pas prêt à utiliser ce résultat, vous devez le stocker pour le renvoyer ultérieurement.

Voici une classe wrapper qui peut être ajoutée à un itérateur existant pour ajouter un test __nonzero__, afin que vous puissiez voir si le générateur est vide avec une simple if. On peut probablement aussi en faire un décorateur.

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)

Voici comment vous l'utiliseriez:

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

Notez que vous pouvez vérifier le vide à tout moment, pas seulement au début de l'itération.

3
Mark Ransom

Je me rends compte que ce poste a 5 ans à ce stade, mais je l’ai trouvé en cherchant un moyen idiomatique de le faire et je n’ai pas vu ma solution publiée. Donc pour la postérité:

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)

Bien sûr, comme de nombreux commentateurs le diront sûrement, il s’agit d’un hacky qui ne fonctionne que dans certaines situations limitées (où les générateurs sont sans effets secondaires, par exemple). YMMV.

3
Real John Connor

Dans mon cas, j’avais besoin de savoir si un ensemble de générateurs était rempli avant de le transmettre à une fonction qui fusionnait les éléments, c.-à-d. Zip(...). La solution est similaire mais assez différente de la réponse acceptée:

Définition:

def has_items(iterable):
    try:
        return True, itertools.chain([next(iterable)], iterable)
    except StopIteration:
        return False, []

Usage:

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"

Mon problème particulier a la propriété que les iterables sont vides ou ont exactement le même nombre d'entrées.

1
André C. Andersen
>>> gen = (i for i in [])
>>> next(gen)
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    next(gen)
StopIteration

À la fin du générateur, StopIteration est levé, puisque dans votre cas la fin est atteinte immédiatement, une exception est levée. Mais normalement, vous ne devriez pas vérifier l'existence de la valeur suivante.

une autre chose que vous pouvez faire est:

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

Je suis juste tombé sur ce fil et j'ai réalisé qu'il manquait une réponse très simple et facile à lire:

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

Si nous ne supposons consommer aucun article, nous devons réinjecter le premier article dans le générateur:

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

Exemple:

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

Utilisez la fonction peek dans 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

L'itérateur renvoyé par cette fonction sera équivalent à celui d'origine passé en argument.

0
W.P. McNeill

Voici un simple décorateur qui enveloppe le générateur, de sorte qu'il ne retourne Aucun s'il est vide. Cela peut être utile si votre code a besoin de savoir si le générateur produira quoi que ce soit avant en boucle.

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

Usage:

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

Cela est utile, par exemple, dans les modèles de code - par exemple, jinja2.

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

Qu'en est-il d'utiliser any ()? Je l'utilise avec des générateurs et ça fonctionne bien. Ici il y a un gars qui explique un peu ce sujet

0
Sam

Invité par Mark Ransom, voici une classe que vous pouvez utiliser pour envelopper tout itérateur de manière à pouvoir regarder en avant. Renvoyez les valeurs dans le flux et vérifiez si elles sont vides. C'est une idée simple avec une implémentation simple que j'ai trouvée très pratique dans le passé.

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)
0
sfkleach

Si vous avez besoin de savoir avant que vous utilisiez le générateur, alors non, il n'y a pas de moyen simple. Si vous pouvez attendre que après que vous ayez utilisé le générateur, il existe un moyen 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()
0
Ethan Furman

avec islice, il vous suffit de vérifier la première itération pour savoir si elle est vide.

depuis itertools import islice

def isempty (iterable):
liste de retour (islice (iterable, 1)) == []

0
Quin

Voici mon approche simple que j'utilise pour continuer à retourner un itérateur tout en vérifiant si quelque chose a été généré

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

Enroulez simplement le générateur avec itertools.chain , mettez quelque chose qui représentera la fin de l'itérable comme deuxième itérable, puis vérifiez-le simplement.

Ex:

import itertools

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

Maintenant, tout ce qui reste à faire est de vérifier la valeur ajoutée à la fin de l'itérable. Lorsque vous la lirez, cela signifiera la fin.

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