2

I want to learn how to use the return value of a generator (but this not what I'm concerned with now).

After searching, they said that I can get the return value from StopIteration when the generator is exhausted, so I tested it with the following code:

def my_generator():
    yield 1
    yield 2
    yield 3
    return "done"


def exhaust_generator(_gen):
    print("===============================================\n")
    print("exhaust_generator")
    try:
        while True:
            print(next(_gen))
    except StopIteration as e:
        print(f"Return value: '{e.value}'")


def exhaust_generator_iter(_gen):
    print("===============================================\n")
    print("exhaust_generator_iter")
    try:
        for i in _gen:
            print(i)
        print(next(_gen))
    except StopIteration as e:
        print(f"Return value: {e.value}")


gen = my_generator()
gen2 = my_generator()
exhaust_generator(gen)
exhaust_generator_iter(gen2)

===============================================

exhaust_generator
1
2
3
Return value: 'done'
===============================================

exhaust_generator_iter
1
2
3
Return value: None

As you can see, the return value is different between the two versions of exhausting the generator and I wonder why.

Searched google but it has not been helpful.

buddemat
  • 4,552
  • 14
  • 29
  • 49
Chicky
  • 185
  • 11

2 Answers2

5

In your first example with the while, you're catching the first StopIteration exception raised by the generator when it's initially exhausted.

In your second example with the for, you're catching a subsequent StopIteration exception raised by calling next() a second time after the generator has already been exhausted (by the for, which caught that initial exception).

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • (As far as I know, there is no way to get that `"done"` return value via a `for`, since the exception gets eaten, but maybe somebody knows an arcane trick that I don't?) – Samwise Mar 30 '23 at 19:23
  • 1
    Oh, I was too slow :) – buddemat Mar 30 '23 at 19:29
  • 1
    [This SO answer](https://stackoverflow.com/a/41875793/14015737) shows a way to get the return value by using a helper function with the `for` loop. – buddemat Mar 30 '23 at 19:42
  • 2
    "yo, I heard you like generators, so I put your generator in a generator!" That does seem like the most graceful way to do it though. – Samwise Mar 30 '23 at 20:09
  • Seems like there's probably a PEP in there somewhere, maybe something like `for ... else as gen_ret:` to be able to capture the generator return at the end of the loop if it's exhausted. – Samwise Mar 30 '23 at 20:15
  • Thank you, I forgot that 'StopIteration' being used to exit the loop – Chicky Mar 31 '23 at 02:07
  • This PEP answers many of my questions about generator but it still like a magic to me. :P https://peps.python.org/pep-0380/ – Chicky Mar 31 '23 at 02:28
2

As far as I understand, the for loop internally also calls next() and eventually catches the StopIteration, so it knows when to stop calling next().

Your next() call in exhaust_generator_iter then raises a second StopIteration but without the passed value.

This SO question and its answers explore ways to receive the return value from a generator, i.a. using a utility class or dependency injection.

buddemat
  • 4,552
  • 14
  • 29
  • 49
  • Thanks for your answer. From what I've searched yesterday it looks like it calls `iter()` to get Iterable (that usually itself) and call `next()` on that Iterable. That make me think that `iter()` is causing the difference I've asked about – Chicky Mar 31 '23 at 02:19