What is generator.throw() good for?

Solution 1:

Let's say I use a generator to handle adding information to a database; I use this to store network-received information, and by using a generator I can do this efficiently whenever I actually receive data, and do other things otherwise.

So, my generator first opens a database connection, and every time you send it something, it'll add a row:

def add_to_database(connection_string):
    db = mydatabaselibrary.connect(connection_string)
    cursor = db.cursor()
    while True:
        row = yield
        cursor.execute('INSERT INTO mytable VALUES(?, ?, ?)', row)

That is all fine and well; every time I .send() my data it'll insert a row.

But what if my database is transactional? How do I signal this generator when to commit the data to the database? And when to abort the transaction? Moreover, it is holding an open connection to the database, maybe I sometimes want it to close that connection to reclaim resources.

This is where the .throw() method comes in; with .throw() I can raise exceptions in that method to signal certain circumstances:

def add_to_database(connection_string):
    db = mydatabaselibrary.connect(connection_string)
    cursor = db.cursor()
    try:
        while True:
            try:
                row = yield
                cursor.execute('INSERT INTO mytable VALUES(?, ?, ?)', row)
            except CommitException:
                cursor.execute('COMMIT')
            except AbortException:
                cursor.execute('ABORT')
    finally:
        cursor.execute('ABORT')
        db.close()

The .close() method on a generator does essentially the same thing; it uses the GeneratorExit exception combined with .throw() to close a running generator.

All this is an important underpinning of how coroutines work; coroutines are essentially generators, together with some additional syntax to make writing a coroutine easier and clearer. But under the hood they are still built on the same yielding, and sending. And when you are running multiple coroutines in parallel, you need a way to cleanly exit those coroutines if one of them has failed, just to name an example.

Solution 2:

In my opinion the throw() method is useful for many reasons.

  1. Symmetry: there is no strong reason for which an exceptional condition should be handled only in the caller and not also in the generator function. (Suppose that a generator reading values from a database returns a bad value, and suppose that only the caller knows that the value is bad. With the throw() method the caller can signal to the generator that there is an abnormal situation that has to be corrected.) If the generator can raise an exception, intercepted by the caller, the reverse should also be possible.

  2. Flexibility: a generator function may have more than one yield statement, and the caller may not be aware of the internal state of the generator. By throwing exceptions it is possible to reset the generator to a known state, or to implement more sophisticated flow control which would be way more cumbersome with next(), send(), close() alone.

An example of resetting the internal state:

def gen():
    try:
        yield 10
        print("State1")
        yield 20
        print("State2")
        yield 30
        print("State3")
    
   except:
        #Reset back to State1!
        yield gen()

g = gen()
print(next(g))
print(next(g))
g = g.throw(ValueError) #state of g has been reset
print(next(g))

>>10
>>State1
>>20
>>10

Asking for use cases may be misleading: for every use case you could produce a counter example without the need for a throw() method, and the discussion would continue forever...

Solution 3:

One use case is to include information about the internal state of a generator in the stack trace when an exception occurs -- information that would not otherwise be visible to the caller.

For example, say we have a generator like the following where the internal state we want is the current index number of the generator:

def gen_items():
    for i, item in enumerate(["", "foo", "", "foo", "bad"]):
        if not item:
            continue
        try:
            yield item
        except Exception:
            raise Exception("error during index: %d" % i)

The following code is not sufficient to trigger the additional exception handling:

# Stack trace includes only: "ValueError: bad value"
for item in gen_items():
    if item == "bad":
        raise ValueError("bad value")

However, the following code does provide the internal state:

# Stack trace also includes: "Exception: error during index: 4"
gen = item_generator()
for item in gen:
    if item == "bad":
        gen.throw(ValueError, "bad value")

Solution 4:

This "answer" is more like a trivia.

We can (ab)use the generator's throw() to raise Exception inside a lambda, which does not otherwise support the raise statement.

foo = lambda: (_ for _ in ()).throw(Exception('foobar'))

Quoted from https://stackoverflow.com/a/8294654/728675