Getting generator return values with natural for loop syntax

Oh my mistake. I guess I got it backwards.

Yeah, the else only catches if there is no break. It makes sense because if it breaks, then we never hit the return statement, so we won’t be able to catch anything in else.

2 Likes

I suspect you’re more likely to find support for adding something like a return_value property[1] to generator objects than for adding else as syntax. With this, your opening example would be more like:

gen = generator()
for value in gen:
    print(value)
else:
    print(gen.return_value)

  1. that raises RuntimeError before return has executed ↩︎

13 Likes

The existing use case for this capability is primarily a historical one: returning values from generators dates from a time when all Python coroutines were just generators with a suitable decorator wrapped around them. It remains in place for backwards compatibility with decorator based alternatives to the native async def coroutine definition syntax.

Personally, if we were to add dedicated syntax for “iterate and get result”, I’d just stick the as clause directly in the main for loop header rather than requiring the else clause:

for value in generator() as result:
    print(value)
print(result)

I’d still be -0 myself (I don’t think the need comes up often enough to add dedicated syntax to handle it). I’d be -1 on altering the expected lifecycle of generator return values by having any references to the exhausted generator also keep the return value alive.

Offering another way to encapsulate this functionality in a utility library if it’s a frequent need in a given application (the resemblance between IterResult and asyncio.Future is not coincidental):

class IterResult:
    def __init__(self):
       self._value_set = False
       self._value = None
    def __repr__(self):
        if self._value_set:
            value = f"value={self._value!r}"
        else:
            value="<value not set>"
        return f"{type(self).__name__}({value})"
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        self._value_set = True
        self._value = value
    def result(self):
        if not self._value_set:
            raise RuntimeError("Iteration result is not set")
        return self.value
def capture_result(iterable):
    itr = iter(iterable)
    result = IterResult()
    def iterator():
        result.value = yield from itr
    return iterator(), result
def generator():
    for i in range(3):
        yield i
    return -1
>>> itr, result = capture_result(generator())
>>> result
IterResult(<value not set>)
>>> result.value
>>> result.result()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 20, in result
RuntimeError: Iteration result is not set
>>> list(itr)
[0, 1, 2]
>>> result
IterResult(value=-1)
>>> result.value
-1
>>> result.result()
-1

Maybe it could make sense to offer something along these lines in itertools (or the third party library more-itertools), but the idiom is rare enough that it doesn’t seem unreasonable to expect projects that use it heavily to define their own helper functions to handle it more neatly.

9 Likes