La mejor manera de recibir el valor de ‘retorno’ de un generador de python

Desde Python 3.3, si una función de generador devuelve un valor, se convierte en el valor para la excepción StopIteration que se genera. Esto se puede recoger de varias maneras:

  • El valor de un yield from expresión, que implica la función de encerramiento, también es un generador.
  • Envolver una llamada a next() o .send() en un bloque try / except.

Sin embargo, si simplemente quiero iterar sobre el generador en un bucle for, la forma más fácil, no parece haber una forma de recostackr el valor de la excepción StopIteration y, por lo tanto, el valor de retorno. Estoy usando un ejemplo simple en el que el generador genera valores y devuelve algún tipo de resumen al final (totales acumulados, promedios, estadísticas de tiempo, etc.).

 for i in produce_values(): do_something(i) values_summary = ....?? 

Una forma es manejar el bucle yo mismo:

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

Pero esto desecha la simplicidad del bucle for. No puedo usar el yield from dado que requiere que el código de llamada sea, en sí mismo, un generador. ¿Hay una forma más sencilla que la de roll-ones-own para el bucle que se muestra arriba?

Resumen de respuestas

Combinando respuestas de @Chad S. y @KT, el más simple parece convertir mi función de generador en una clase usando el protocolo de iterador:

 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 

Y la respuesta de @Ferdinand Beyer es más simple si no puedo refactorizar al productor de valor.

Puede pensar en el atributo de value de StopIteration (y posiblemente en el mismo StopIteration ) como detalles de implementación, no diseñados para ser utilizados en el código “normal”.

Eche un vistazo a la PEP 380 que especifica el yield from característica de Python 3.3: Se analiza que algunas alternativas de usar StopIteration para llevar el valor de retorno donde se considera.

Como no se supone que obtenga el valor de retorno en un bucle ordinario, no hay syntax para ello. De la misma forma en que se supone que no debe capturar la StopIteration explícitamente.

Una buena solución para su situación sería una pequeña clase de utilidad (podría ser lo suficientemente útil para la biblioteca estándar):

 class Generator: def __init__(self, gen): self.gen = gen def __iter__(self): self.value = yield from self.gen 

Esto envuelve cualquier generador y captura su valor de retorno para ser inspeccionado más adelante:

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

Podría hacer un envoltorio de ayuda, que atraparía el StopIteration y extraería el valor para usted:

 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) 

El método más obvio que se me ocurre para esto sería un tipo definido por el usuario que recordaría el resumen para usted.

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

Una forma ligera de manejar el valor de retorno (una que no implique la creación de una instancia de una clase auxiliar) es usar la dependency injection .

A saber, uno puede pasar la función para manejar / actuar sobre el valor de retorno usando la siguiente función de generador de contenedor / ayudante:

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

Por ejemplo, el siguiente–

 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) 

resultados en–

 1 2 returned: 3