web-dev-qa-db-fra.com

Comment le rendement intercepte l'exception StopIteration?

Pourquoi dans l'exemple la fonction se termine:

def func(iterable):
    while True:
        val = next(iterable)
        yield val

mais si je retire la fonction de déclaration de rendement, une exception StopIteration sera levée?

EDIT: Désolé de vous avoir induit en erreur. Je sais ce que sont les générateurs et comment les utiliser. Bien sûr, quand j'ai dit que la fonction se termine, je ne voulais pas dire une évaluation enthousiaste de la fonction. Je viens de laisser entendre que lorsque j'utilise la fonction pour produire un générateur:

gen = func(iterable)

en cas de func cela fonctionne et retourne le même générateur, mais en cas de func2:

def func2(iterable):
    while True:
        val = next(iterable)

il lève StopIteration au lieu de Aucun return ou boucle infinie.

Permettez-moi d'être plus précis. Il existe une fonction tee dans itertools qui équivaut à:

def tee(iterable, n=2):
    it = iter(iterable)
    deques = [collections.deque() for i in range(n)]
    def gen(mydeque):
        while True:
            if not mydeque:             # when the local deque is empty
                newval = next(it)       # fetch a new value and
                for d in deques:        # load it to all the deques
                    d.append(newval)
            yield mydeque.popleft()
    return Tuple(gen(d) for d in deques)

Il y a, en fait, de la magie, car la fonction imbriquée gen a une boucle infinie sans instructions break. gen la fonction se termine en raison de l'exception StopIteration lorsqu'il n'y a aucun élément dans it. Mais il se termine correctement (sans lever d'exceptions), c'est-à-dire arrête simplement la boucle. La question est donc : où est StopIteration est géré?

23
Sergey Ivanov

Pour répondre à votre question sur l'emplacement de StopIteration dans le générateur gen créé à l'intérieur de itertools.tee: Ce n'est pas le cas. Il appartient au consommateur des résultats tee d'attraper l'exception lors de leur itération.

Tout d'abord, il est important de noter qu'une fonction de générateur (qui est n'importe quelle fonction contenant une instruction yield, n'importe où) est fondamentalement différente d'une fonction normale. Au lieu d'exécuter le code de la fonction lors de son appel, vous obtiendrez simplement un objet generator lorsque vous appelez la fonction. Ce n'est que lorsque vous parcourez le générateur que vous exécutez le code.

Une fonction de générateur ne terminera jamais l'itération sans lever StopIteration (à moins qu'elle ne déclenche une autre exception à la place). StopIteration est le signal du générateur que c'est fait, et ce n'est pas optionnel. Si vous atteignez une instruction return ou la fin du code de la fonction du générateur sans générer quoi que ce soit, Python lèvera StopIteration pour vous!

Ceci est différent des fonctions régulières, qui renvoient None si elles atteignent la fin sans retourner autre chose. Cela correspond aux différentes façons dont les générateurs fonctionnent, comme je l'ai décrit ci-dessus.

Voici un exemple de fonction de générateur qui vous permettra de voir facilement comment StopIteration est généré:

def simple_generator():
    yield "foo"
    yield "bar"
    # StopIteration will be raised here automatically

Voici ce qui se passe lorsque vous en consommez:

>>> g = simple_generator()
>>> next(g)
'foo'
>>> next(g)
'bar'
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    next(g)
StopIteration

L'appel de simple_generator Renvoie toujours un objet generator immédiatement (sans exécuter le code de la fonction). Chaque appel de next sur l'objet générateur exécute le code jusqu'à l'instruction yield suivante et renvoie la valeur renvoyée. S'il n'y a plus rien à obtenir, StopIteration est levé.

Maintenant, normalement, vous ne voyez pas d'exceptions StopIteration. La raison en est que vous consommez généralement des générateurs dans les boucles for. Une instruction for appellera automatiquement next plusieurs fois jusqu'à ce que StopIteration soit levé. Il interceptera et supprimera l'exception StopIteration pour vous, vous n'avez donc pas besoin de jouer avec les blocs try/except pour y faire face.

Une boucle for comme for item in iterable: do_suff(item) est presque exactement équivalente à cette boucle while (la seule différence étant qu'une vraie for n'a pas besoin d'une variable temporaire pour tenir l'itérateur):

iterator = iter(iterable)
try:
    while True:
        item = next(iterator)
        do_stuff(item)
except StopIteration:
    pass
finally:
    del iterator

La fonction de générateur gen que vous avez montrée en haut est une exception. Il utilise l'exception StopIteration produite par l'itérateur qu'il consomme car c'est son propre signal qu'il a fini d'être itéré. Autrement dit, plutôt que d'attraper le StopIteration puis de sortir de la boucle, il laisse simplement l'exception non détectée (probablement pour être interceptée par un code de niveau supérieur).

Sans rapport avec la question principale, il y a une autre chose que je veux souligner. Dans votre code, vous appelez next sur une variable appelée iterable. Si vous prenez ce nom comme documentation pour le type d'objet que vous obtiendrez, ce n'est pas nécessairement sûr.

next fait partie du protocole iterator, pas du protocole iterable (ou container). Cela peut fonctionner pour certains types d'itérables (tels que les fichiers et les générateurs, car ces types sont leurs propres itérateurs), mais il échouera pour d'autres itérables, tels que les tuples et les listes. L'approche la plus correcte consiste à appeler iter sur votre valeur iterable, puis à appeler next sur l'itérateur que vous recevez. (Ou utilisez simplement les boucles for, qui appellent à la fois iter et next pour vous!)

Edit: Je viens de trouver ma propre réponse dans une recherche Google pour une question connexe, et je pensais que je mettrais à jour pour souligner que la réponse ci-dessus ne sera pas complètement vraie dans les futures versions Python. PEP 479 fait une erreur pour permettre à un StopIteration de bouillonner sans être attrapé par une fonction de générateur. Si cela se produit, Python le transformera en une exception RuntimeError à la place.

Cela signifie que le code comme les exemples de itertools qui utilisent un StopIteration pour sortir d'une fonction de générateur devra être modifié. Habituellement, vous devrez intercepter l'exception avec un try/except puis faire return.

Parce qu'il s'agit d'un changement incompatible en amont, il est progressivement mis en place. Dans Python 3.5, tout le code fonctionnera comme avant par défaut, mais vous pouvez obtenir le nouveau comportement avec from __future__ import generator_stop. Dans Python 3.6, le code fonctionnera toujours, mais il donnera un avertissement. Dans Python 3.7, le nouveau comportement s'appliquera tout le temps.

42
Blckknght

Lorsqu'une fonction contient yield, l'appeler n'exécute rien, elle crée simplement un objet générateur. Seule l'itération sur cet objet exécutera le code. Donc, je suppose que vous appelez simplement la fonction, ce qui signifie que la fonction n'élève pas StopIteration parce que elle n'est jamais en cours d'exécution.

Compte tenu de votre fonction, et d'un itérable:

def func(iterable):
    while True:
        val = next(iterable)
        yield val

iterable = iter([1, 2, 3])

C'est la mauvaise façon de l'appeler:

func(iterable)

C'est le bon chemin:

for item in func(iterable):
    # do something with item

Vous pouvez également stocker le générateur dans une variable et appeler next() dessus (ou itérer dessus d'une autre manière):

gen = func(iterable)
print(next(gen))   # prints 1
print(next(gen))   # prints 2
print(next(gen))   # prints 3
print(next(gen))   # StopIteration

Soit dit en passant, une meilleure façon d'écrire votre fonction est la suivante:

def func(iterable):
    for item in iterable:
        yield item

Ou en Python 3.3 et versions ultérieures:

def func(iterable):
    yield from iter(iterable)

Bien sûr, les vrais générateurs sont rarement aussi triviaux. :-)

6
kindall

Sans yield, vous parcourez l'intégralité de iterable sans vous arrêter pour faire quoi que ce soit avec val. La boucle while ne capture pas l'exception StopIteration. Une boucle for équivalente serait:

def func(iterable):
    for val in iterable:
        pass

qui attrape le StopIteration et quitte simplement la boucle et revient ainsi de la fonction.

Vous pouvez explicitement intercepter l'exception:

def func(iterable):
    while True:
        try:
            val = next(iterable)
        except StopIteration:
            break
3
chepner

yield n'attrape pas le StopIteration. Ce que yield fait pour votre fonction, c'est qu'elle la fait devenir une fonction de générateur plutôt qu'une fonction régulière. Ainsi, l'objet renvoyé par l'appel de fonction est un objet itérable (qui calcule la valeur suivante lorsque vous le lui demandez avec la fonction next (qui est appelée implicitement par une boucle for)). Si vous en laissez l'instruction yield, alors python exécute la boucle while tout de suite, ce qui finit par épuiser l'itérable (s'il est fini) et augmenter StopIteration juste quand vous l'appelez.

considérer:

x = func(x for x in [])
next(x)  #raises StopIteration

Une boucle for intercepte l'exception - C'est ainsi qu'elle sait quand arrêter d'appeler next sur l'itérable que vous lui avez donné.

0
mgilson