web-dev-qa-db-fra.com

Meilleure façon de recevoir la valeur de retour d'un générateur python

Puisque Python 3.3, si une fonction de générateur renvoie une valeur, cela devient la valeur de l'exception StopIteration qui est déclenchée. Cela peut être collecté de plusieurs façons:

  • La valeur d'une expression yield from, Ce qui implique que la fonction englobante est également un générateur.
  • Envelopper un appel à next() ou .send() dans un bloc try/except.

Cependant, si je veux simplement parcourir le générateur dans une boucle for - la manière la plus simple - il ne semble pas y avoir de moyen de collecter la valeur de l'exception StopIteration, et donc la valeur de retour. Im en utilisant un exemple simple où le générateur donne des valeurs et renvoie une sorte de résumé à la fin (totaux cumulés, moyennes, statistiques de synchronisation, etc.).

for i in produce_values():
    do_something(i)

values_summary = ....??

Une façon consiste à gérer la boucle moi-même:

values_iter = produce_values()
try:
    while True:
        i = next(values_iter)
        do_something(i)
except StopIteration as e:
    values_summary = e.value

Mais cela gâche la simplicité de la boucle for. Je ne peux pas utiliser yield from Car cela nécessite que le code appelant soit lui-même un générateur. Existe-t-il un moyen plus simple que la boucle roll-ones-own pour la boucle illustrée ci-dessus?

Résumé de la réponse

En combinant les réponses de @Chad S. et @KT, la plus simple semble transformer ma fonction de générateur en classe en utilisant le protocole itérateur:

class ValueGenerator():
    def __iter__(self):
        yield 1
        yield 2
        # and so on
        self.summary = {...}

vg = ValueGenerator()
for i in vg:
    do_something(i)
values_summary = vg.summary

Et la réponse de @Ferdinand Beyer est la plus simple si je ne peux pas refactoriser le producteur de valeur.

27
Chris Cogdon

Vous pouvez considérer l'attribut value de StopIteration (et sans doute StopIteration lui-même) comme des détails d'implémentation, non conçus pour être utilisés dans du code "normal".

Jetez un oeil à PEP 38 qui spécifie le yield from fonctionnalité de Python 3.3: Il explique que certaines alternatives d'utilisation de StopIteration pour transporter la valeur de retour sont envisagées.

Comme vous n'êtes pas censé obtenir la valeur de retour dans une boucle for ordinaire, il n'y a pas de syntaxe pour cela. De la même manière que vous n'êtes pas censé attraper explicitement le StopIteration.

Une bonne solution pour votre situation serait une petite classe d'utilité (pourrait être assez utile pour la bibliothèque standard):

class Generator:
    def __init__(self, gen):
        self.gen = gen

    def __iter__(self):
        self.value = yield from self.gen

Cela enveloppe tout générateur et capture sa valeur de retour pour être inspecté plus tard:

>>> def test():
...     yield 1
...     return 2
...
>>> gen = Generator(test())
>>> for i in gen:
...    print(i)
...
1
>>> print(gen.value)
2
15
Ferdinand Beyer

Vous pourriez créer un wrapper d'aide, qui intercepterait le StopIteration et en extrairait la valeur pour vous:

from functools import wraps

class ValueKeepingGenerator(object):
    def __init__(self, g):
        self.g = g
        self.value = None
    def __iter__(self):
        self.value = yield from self.g

def keep_value(f):
    @wraps(f)
    def g(*args, **kwargs):
        return ValueKeepingGenerator(f(*args, **kwargs))
    return g

@keep_value
def f():
    yield 1
    yield 2
    return "Hi"

v = f()
for x in v:
    print(x)

print(v.value)
10
KT.

Une façon légère de gérer la valeur de retour (qui n'implique pas l'instanciation d'une classe auxiliaire) consiste à utiliser injection de dépendance .

A savoir, on peut passer la fonction pour gérer/agir sur la valeur de retour en utilisant la fonction de générateur wrapper/helper suivante:

def handle_return(generator, func):
    returned = yield from generator
    func(returned)

Par exemple, ce qui suit ...

def generate():
    yield 1
    yield 2
    return 3

def show_return(value):
    print('returned: {}'.format(value))

for x in handle_return(generate(), show_return):
    print(x)

résulte en--

1
2
returned: 3
5
cjerdonek

La méthode la plus évidente à laquelle je peux penser serait un type défini par l'utilisateur qui se souviendrait du résumé pour vous.

>>> import random
>>> class ValueProducer:
...    def produce_values(self, n):
...        self._total = 0
...        for i in range(n):
...           r = random.randrange(n*100)
...           self._total += r
...           yield r
...        self.value_summary = self._total/n
...        return self.value_summary
... 
>>> v = ValueProducer()
>>> for i in v.produce_values(3):
...    print(i)
... 
25
55
179
>>> print(v.value_summary)
86.33333333333333
>>> 
3
Chad S.