Best way to receive the 'return' value from a python generator
Question: Since Python 3.3, if a generator function returns a value, that becomes the
value for the StopIteration exception that is raised. This can be collected a
number of ways:

  * The value of a `yield from` expression, which implies the enclosing function is also a generator.
  * Wrapping a call to `next()` or `.send()` in a try/except block.

However, if I'm simply wanting to iterate over the generator in a for loop -
the easiest way - there doesn't appear to be a way to collect the value of the
StopIteration exception, and thus the return value. Im using a simple example
where the generator yields values, and returns some kind of summary at the end
(running totals, averages, timing statistics, etc).

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

One way is to handle the loop myself:

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

But this throws away the simplicity of the for loop. I can't use `yield from`
since that requires the calling code to be, itself, a generator. Is there a
simpler way than the roll-ones-own for loop shown above?

## Answer summary

Combining answers from @Chad S. and @KT, the simplest appears to turn my
generator function into a class using the iterator protocol:

    
    
    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
    

And @Ferdinand Beyer's answer is simplest if I can't refactor the value
producer.


Answer: You could make a helper wrapper, that would catch the `StopIteration` and
extract the value for you:

    
    
    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)
    

