Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect in a generator that it has been interrupted from outside

I have this generator:

def gen():
    rounds = 0
    for x in xrange(10):
        rounds += 1
        yield x
    print 'Generator finished (%d rounds)' % (rounds)

If I call it normally:

for x in gen():
    pass

I get the expected:

Generator finished (10 rounds)

But if I interrupt the generator, I get nothing:

for x in gen():
    break

I would like to get:

Generator interrupted (1 rounds)

Is it possible to detect, in the generator itself, that it has been interrupted from outside? Is there any Exception that I could catch to detect this event?

like image 859
blueFast Avatar asked Jan 09 '23 17:01

blueFast


1 Answers

You cannot, because a generator's default behaviour is to be interrupted all the time. A generator is constantly paused, every time it yields a value.

In other words, your understanding of how generators work is incorrect. The looping over a generator is entirely outside the control of the generator; all it is tasked to do is produce a next value, which unpauses the code until the next yield expression is executed.

As such, when no next value is being requested, the generator is paused and cannot execute code to 'detect' that it is not being asked for another value.

What happens when you call a generator function is this:

  • A special generator iterator object is returned. No code in the function body is executed.
  • Code outside the generator iterator may or may not use it as an iterator. Each time a new value is needed, generator_object.next() is called.
  • When the .next() method is called, the function body is run, until a yield expression is encountered. The function body is paused and the result of the yield expression is returned as the result of the .next() method.

You can explicitly send messages or raise exceptions in your generator function with the generator_object.send() and generator_object.throw() methods, but no such messages or exceptions are sent when not iterating over the generator object.

One thing you could look for is the GeneratorExit exception thrown inside your generator function when the generator object is closed; see the generator_object.close() method:

Raises a GeneratorExit at the point where the generator function was paused

When a generator object is garbage collected (e.g. nothing references it anymore) the generator_object.close() method is called automatically:

def gen():
    rounds = 0
    for x in xrange(10):
        rounds += 1
        try:
            yield x
        except GeneratorExit:
            print 'Generator closed early after %d rounds' % rounds
            raise
    print 'Generator finished (%d rounds)' % rounds

Demo:

>>> def gen():
...     rounds = 0
...     for x in xrange(10):
...         rounds += 1
...         try:
...             yield x
...         except GeneratorExit:
...             print 'Generator closed early after %d rounds' % rounds
...             raise
...     print 'Generator finished (%d rounds)' % rounds
... 
>>> for i in gen():
...     break
... 
Generator closed early after 1 rounds

This only works because the returned generator object is only referenced by the for loop, and is reaped the moment the for loop ends.

like image 183
Martijn Pieters Avatar answered Jan 12 '23 06:01

Martijn Pieters