Generator and return value

Steve D'Aprano steve+python at pearwood.info
Wed Jun 7 05:19:38 EDT 2017


On Wed, 7 Jun 2017 05:09 pm, Jussi Piitulainen wrote:

> Frank Millman writes:
> 
>> It would be nice to write a generator in such a way that, in addition
>> to 'yielding' each value, it performs some additional work and then
>> 'returns' a final result at the end.
>>
>>> From Python 3.3, anything 'returned' becomes the value of the
>>> StopIteration
>> exception, so it is possible, but not pretty.
>>
>> Instead of -
>>    my_gen = generator()
>>    for item in my_gen():
>>        do_something(item)
>>    [how to get the final result?]


Currently, I don't believe there is a way.


>> you can write -
>>    my_gen = generator()
>>    while True:
>>        try:
>>            item = next(my_gen())
>>            do_something(item)
>>        except StopIteration as e:
>>            final_result = e.value
>>
>> Is this the best way to achieve it, or is there a nicer alternative?

I don't think there are currently any nice alternatives.



> Like this, and imagination is the limit:
> 
> def generator(box):
>     yield 1
>     box.append('won')
>     yield 2
>     box.append('too')
>     yield 3
>     box.append('tree')
> 
> mabox = []
> for item in generator(mabox): pass
> print(*mabox)
> # prints: won too tree

Well, that's a little bit better than using a global variable, but it's still
pretty ugly, and it doesn't capture the return value so you've answered a
completely different question.


Here is why I think it is an ugly solution. There are three obvious alternatives
to this API:


Alternative one: force the caller to provide the "box" argument, whether they
care about these extra values or not.

    for item in generator([]):  # argument is ignored, and garbage collected
        process(item)

That's not *too* bad, but still a bit manky.


Alternative two: provide a default value for box:

    def generator(box=[]): ...


Pros: now the caller doesn't need to care about box if they don't care about it.

Cons: it will leak memory. Every call to generator() with no box argument will
collect values in the default list.


Alternative three: provide a non-mutable default for box:

    def generator(box=None): ...


Procs: the caller doesn't need to care about box.

Cons: writing generator is a lot more complex, you have to check for box is None
before appending. There may be clever ways around this, but either way, the
complexity of the generator is significantly increased.


I'm not saying I'd *never* use this solution. I've used a similar solution
myself, treating the argument as a "pass by reference" output parameter,
similar to "var" arguments in Pascal. But not often, because it feels ugly.

Otherwise, I guess using a while loop is the least-worst existing solution. But
here's a neat solution for a feature request:


# this doesn't actually work, yet
it = generator()
for value in it:
    # run the generator through to completion, as normal
    process(value)

extra_value = it.result

where the property lookup it.result:


- captures the return value, if the generator has returned;

- raise an exception if the generator is still running.



-- 
Steve
“Cheer up,” they said, “things could be worse.” So I cheered up, and sure
enough, things got worse.




More information about the Python-list mailing list