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


146

Est - il un moyen simple de tester si le générateur n'a pas d' éléments, comme peek, hasNext, isEmpty, quelque chose dans ce sens?


Corrigez-moi si je me trompe, mais si vous pouviez apporter une solution vraiment générique à n'importe quel générateur, ce serait l'équivalent de définir des points d'arrêt sur les instructions yield et d'avoir la capacité de "reculer". Cela signifierait-il cloner le cadre de la pile sur les rendements et les restaurer sur StopIteration?

Eh bien, je suppose que les restaurer StopIteration ou non, mais au moins StopIteration vous dirait qu'il était vide. Ouais, j'ai besoin de dormir ...

4
Je pense que je sais pourquoi il veut ça. Si vous faites du développement Web avec des modèles et que vous passez la valeur de retour dans un modèle comme Cheetah ou quelque chose du genre, la liste vide []est idéalement Falsey afin que vous puissiez faire une vérification si et faire un comportement spécial pour quelque chose ou rien. Les générateurs sont vrais même s'ils ne donnent aucun élément.
jpsimons

Voici mon cas d'utilisation ... J'utilise glob.iglob("filepattern")un modèle générique fourni par l'utilisateur et je souhaite avertir l'utilisateur si le modèle ne correspond à aucun fichier. Bien sûr, je peux contourner ce problème de différentes manières, mais il est utile de pouvoir tester proprement si l'itérateur est venu vide ou non.
LarsH

Peut être utiliser cette solution: stackoverflow.com/a/11467686/463758
balki

Réponses:


53

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

Il ne devrait vraiment 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 garder la séquence en mémoire . Il n'y a donc pas de traversée en arrière.

Vous pouvez écrire une fonction has_next ou peut-être même l'appliquer à un générateur comme méthode avec un décorateur sophistiqué si vous le souhaitez.


2
assez juste, cela a du sens. Je savais qu'il n'y avait aucun moyen de trouver la longueur d'un générateur, mais je pensais que j'aurais peut-être manqué un moyen de savoir s'il allait initialement générer quoi que ce soit.
Dan

1
Oh, et pour référence, j'ai essayé d'implémenter ma propre suggestion de "décorateur fantaisie". DUR. Apparemment, copy.deepcopy ne fonctionne pas sur les générateurs.
David Berger

47
Je ne suis pas sûr de pouvoir être d'accord avec "il ne devrait pas y avoir de moyen simple". Il y a beaucoup d'abstractions en informatique qui sont conçues pour sortir une séquence de valeurs sans garder la séquence en mémoire, mais qui permettent au programmeur de demander s'il y a une autre valeur sans la retirer de la «file d'attente» s'il y en a. Il y a une telle chose comme un simple coup d'oeil sans nécessiter de "traversée en arrière". Cela ne veut pas dire qu'une conception d'itérateur doit fournir une telle fonctionnalité, mais c'est certainement utile. Peut-être que vous vous opposez au fait que la première valeur pourrait changer après le coup d'oeil?
LarsH

9
Je m'oppose au motif qu'une implémentation typique ne calcule même pas une valeur tant qu'elle n'est pas nécessaire. On pourrait forcer l'interface à faire cela, mais cela pourrait être sous-optimal pour les implémentations légères.
David Berger

6
@ S.Lott vous n'avez pas besoin de générer la séquence entière pour savoir si la séquence est vide ou non. La valeur de stockage d'un élément est suffisante - voir ma réponse.
Mark Ransom

99

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.

2
Je ne comprends pas tout à fait l'intérêt de renvoyer le premier élément deux fois return first, itertools.chain([first], rest).
njzk2

6
@ njzk2 J'allais pour une opération "peek" (d'où le nom de la fonction). wiki "Peek est une opération qui renvoie la valeur du haut de la collection sans supprimer la valeur des données"
John Fouhy

Cela ne fonctionnera pas si le générateur est conçu pour ne produire aucun. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

4
@Paul Regardez attentivement les valeurs de retour. Si le générateur est terminé - c'est-à-dire qu'il ne retourne pas Nonemais augmente StopIteration- le résultat de la fonction est None. Sinon, c'est un tuple, ce qui ne l'est pas None.
Fund Monica's Lawsuit

Cela m'a beaucoup aidé dans mon projet actuel. J'ai trouvé un exemple similaire dans le code du module de bibliothèque standard de 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
pair

29

Une manière simple est d'utiliser le paramètre optionnel pour 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.


1
Non. Ceci est incorrect pour tout générateur dont la première valeur fournie est fausse.
mehtunguh

7
Utilisez un object()lieu de classpour en faire une ligne plus courte: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

Ou remplacez, Nonemais quelle que soit la valeur que vous connaissez, ce n'est pas dans votre générateur.

Edit : Oui, cela sautera 1 élément dans le générateur. Souvent, cependant, je vérifie si un générateur est vide uniquement à des fins de validation, alors je ne l'utilise pas vraiment. Ou sinon je fais quelque chose comme:

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

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

Autrement dit, cela fonctionne si votre générateur provient d'une fonction , comme dans generator().


4
Pourquoi ce n'est pas la meilleure réponse? Au cas où le générateur reviendrait None?
Sait le

8
Probablement parce que cela vous oblige à consommer réellement le générateur au lieu de simplement tester s'il est vide.
bfontaine

3
C'est mauvais car au moment où vous appelez le prochain (générateur, Aucun), vous sauterez 1 élément s'il est disponible
Nathan Do

Correct, vous allez manquer le 1er élément de votre génération et vous allez également consommer votre génération plutôt que de tester si elle est vide.
AJ

12

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, thingcontiendra la dernière valeur générée. Si rien n'a été généré, il sera indéfini - sauf si vous avez 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!"

3
Cette solution essaiera de consommer tout le générateur le rendant ainsi inutilisable pour des générateurs infinis.
Viktor Stískala

@ ViktorStískala: Je ne vois pas votre point. Il serait insensé de tester si un générateur infini produisait des résultats.
vezult

Je voulais souligner que votre solution pourrait contenir une rupture dans la boucle for, car vous ne traitez pas les autres résultats et il est inutile qu'ils soient générés. range(10000000)est un générateur fini (Python 3), mais vous n'avez pas besoin de parcourir tous les éléments pour savoir s'il génère quelque chose.
Viktor Stískala

1
@ ViktorStískala: Compris. Cependant, mon point est le suivant: généralement, vous voulez réellement fonctionner sur la sortie du générateur. Dans mon exemple, si rien n'est généré, vous le savez maintenant. Sinon, vous opérez sur la sortie générée comme prévu - "L'utilisation du générateur est le test". Pas besoin de tests spéciaux, ni de consommer inutilement la puissance du générateur. J'ai modifié ma réponse pour clarifier cela.
vezult

8

Je déteste offrir une deuxième solution, en particulier celui que je ne me utiliser, mais, si vous absolument deviez le faire et de ne pas consommer 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, car je pense que ce n'est pas ainsi que les générateurs doivent être utilisés.


4

Je me rends compte que ce post a 5 ans à ce stade, mais je l'ai trouvé en cherchant une manière idiomatique de le faire, et je n'ai pas vu ma solution posté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 je suis sûr que de nombreux commentateurs le souligneront, c'est piraté et ne fonctionne que dans certaines situations limitées (où les générateurs sont sans effets secondaires, par exemple). YMMV.


1
Cela n'appellera le gengénérateur qu'une seule fois pour chaque élément, donc les effets secondaires ne sont pas un trop gros problème. Mais il stockera une copie de tout ce qui a été extrait du générateur via b, mais pas via a, donc les implications de mémoire sont similaires à une simple exécution list(gen)et à une vérification.
Matthias Fripp

Il a deux problèmes. 1. Cet outil informatique peut nécessiter un stockage auxiliaire important (en fonction de la quantité de données temporaires à stocker). En général, si un itérateur utilise la plupart ou toutes les données avant qu'un autre itérateur ne démarre, il est plus rapide d'utiliser list () au lieu de tee (). 2. Les itérateurs de tee ne sont pas threadsafe. Une RuntimeError peut être déclenchée lors de l'utilisation simultanée d'itérateurs retournés par le même appel tee (), même si l'itérable d'origine est threadsafe.
AJ

3

Désolé pour l'approche évidente, mais la meilleure façon serait de faire:

for item in my_generator:
     print item

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

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


Ou ... l'interlocuteur pourrait donner une idée de pourquoi on essaierait de détecter un générateur vide?
S.Lott

vouliez-vous dire "rien ne sera affiché car le générateur est vide"?
SilentGhost

S.Lott. Je suis d'accord. Je ne vois pas pourquoi. Mais je pense que même s'il y avait une raison, le problème serait peut-être mieux tourné pour utiliser chaque élément à la place.
Ali Afshar

1
Cela n'indique pas au programme si le générateur était vide.
Ethan Furman

3

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 plus tard.

Voici une classe wrapper qui peut être ajoutée à un itérateur existant pour ajouter un __nonzero__test, afin que vous puissiez voir si le générateur est vide avec un simple if. Il peut probablement aussi être transformé en 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 la vacuité à tout moment, pas seulement au début de l'itération.


Cela va dans la bonne direction. Il doit être modifié pour permettre de jeter un œil aussi loin que vous le souhaitez, en stockant autant de résultats que nécessaire. Idéalement, cela permettrait de pousser des éléments arbitraires sur la tête du flux. Un itérateur poussable est une abstraction très utile que j'utilise souvent.
sfkleach

@sfkleach Je ne vois pas la nécessité de compliquer cela pour un aperçu multiple, c'est assez utile en l'état et répond à la question. Même s'il s'agit d'une vieille question, elle reçoit encore un coup d'œil occasionnel, donc si vous voulez laisser votre propre réponse, quelqu'un pourrait la trouver utile.
Mark Ransom

Mark a tout à fait raison de dire que sa solution répond à la question, qui est le point clé. J'aurais dû mieux le formuler. Ce que je voulais dire, c'est que les itérateurs poussables avec un pushback illimité sont un idiome que j'ai trouvé extrêmement utile et que la mise en œuvre est sans doute encore plus simple. Comme suggéré, je publierai le code de la variante.
sfkleach

2

À la demande de Mark Ransom, voici une classe que vous pouvez utiliser pour encapsuler n'importe quel itérateur afin que vous puissiez jeter un coup d'œil à l'avance, repousser les valeurs dans le flux et vérifier qu'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)

2

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 sommes censés consommer aucun élément, nous devons réinjecter le premier élément 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
>>> 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 StopIterationest déclenché, car dans votre cas, la fin est immédiatement atteinte, l'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')

2
Ce qui consomme en fait tout le générateur. Malheureusement, il n'est pas clair d'après la question s'il s'agit d'un comportement souhaitable ou indésirable.
S.Lott

comme toute autre façon de "toucher" le générateur, je suppose.
SilentGhost

Je me rends compte que c'est vieux, mais utiliser 'list ()' ne peut pas être le meilleur moyen, si la liste générée n'est pas vide mais en fait grande, alors c'est inutilement inutile
Chris_Rands

1

Si vous avez besoin de savoir avant d'utiliser le générateur, alors non, il n'y a pas de moyen simple. Si vous pouvez attendre après avoir 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()

1

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

Ex:

import itertools

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

Il ne reste plus qu'à vérifier cette valeur que nous avons ajoutée à la fin de l'itérable, lorsque vous le 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

Utilisez eog = object()au lieu de supposer que float('-inf')cela ne se produira jamais dans l'itérable.
bfontaine

@bfontaine Good idea
smac89

1

Dans mon cas, j'avais besoin de savoir si une foule de générateurs était remplie avant de la transmettre à une fonction, qui fusionnait les éléments, c'est-à-dire 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 itérables sont vides ou ont exactement le même nombre d'entrées.


1

Je n'ai trouvé que cette solution fonctionnant également pour les itérations vides.

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 si vous ne souhaitez pas utiliser d'exception pour cela, essayez d'utiliser

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)

Dans la solution marquée, vous ne pouvez pas l'utiliser pour des générateurs vides comme

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Voici mon approche simple que j'utilise pour continuer à renvoyer un itérateur tout en vérifiant si quelque chose a été produit.Je vérifie simplement si la boucle s'exécute:

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

0

Voici un décorateur simple qui enveloppe le générateur, il renvoie donc None s'il est vide. Cela peut être utile si votre code a besoin de savoir si le générateur produira quelque chose avant de le parcourir.

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

Un exemple où cela est utile est dans la création de modèles de code - c'est-à-dire jinja2

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

Cela appelle la fonction du générateur deux fois, ce qui entraînera le coût de démarrage du générateur deux fois. Cela pourrait être substantiel si, par exemple, la fonction de générateur est une requête de base de données.
Ian Gold du

0

en utilisant islice, vous n'avez qu'à vérifier jusqu'à la première itération pour découvrir s'il est vide.

à partir d'itertools importer islice

def isempty (iterable):
    return list (islice (iterable, 1)) == []


Désolé, c'est une lecture consommatrice ... Je dois faire l'essai / attraper avec StopIteration
Quin

0

Qu'en est-il de l'utilisation de any ()? Je l'utilise avec des générateurs et ça marche très bien. Ici, il y a un gars qui explique un peu à ce sujet


2
Nous ne pouvons pas utiliser "any ()" pour tout le générateur. J'ai juste essayé de l'utiliser avec un générateur contenant plusieurs dataframes. J'ai reçu ce message "La valeur de vérité d'un DataFrame est ambiguë." sur any (my_generator_of_df)
probitaille

any(generator)fonctionne lorsque vous savez que le générateur générera des valeurs qui peuvent être converties en bool- les types de données de base (par exemple, int, string) fonctionnent. any(generator)sera False lorsque le générateur est vide, ou lorsque le générateur n'a que de fausses valeurs - par exemple, si un générateur va générer 0, `` (chaîne vide) et False, alors il sera toujours False. Cela peut ou non être le comportement prévu, du moment que vous en êtes conscient :)
Daniel

0

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 retourné par cette fonction sera équivalent à l'itérateur d'origine passé en argument.


-2

Je l'ai résolu en utilisant la fonction somme. Voir ci-dessous un exemple que j'ai utilisé avec glob.iglob (qui renvoie un générateur).

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

* Cela ne fonctionnera probablement pas pour les générateurs ÉNORMES mais devrait bien fonctionner pour les petites listes

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.