Thinking more deeply about what will happen in a more complex case, the view justifies the choice of Python, avoiding bubble exceptions from the generator.
If I had an I / O error from a stream object, the probability of just recovering and continuing reading would be if the structures local to the reset generator were somehow low. I would have to put up with the reading process in order to continue: skip garbage, discard partial data, reset an incorrect internal tracking structure, etc.
Only the generator has enough context to do it right. Even if you could keep the context of the generator with an external block, the exceptions would completely violate the Law of Demeter. All the important information that the surrounding block should reset and move on is in the local variables of the generator function! And receiving or transmitting this information, although possible, is disgusting.
The resulting exception will almost always be selected after cleaning, in which case the reader generator will already have an internal exception block. Trying very hard to maintain this cleanliness in a simple case with the brain, just so that it crashes in almost any realistic context, would be stupid. So just try in the generator, you still need the body of the except block in any difficult case.
It would be nice if the exceptional conditions could look like exceptions, but not like return values. Therefore, I would add an intermediate adapter to allow this: the generator throws either data or exceptions, and the adapter could raise the exception again, if applicable. The adapter must be named first in the for loop, so we have the opportunity to catch it in the loop and clear it before continuing, or exit the loop to catch it and abandon the process. And we need to put some kind of lame shell around the installation to indicate that the tricks are on, and make the adapter receive a call if the function adapts.
Thus, each layer represents errors with which it has a context for processing, due to the fact that the adapter is a tiny bit intrusive (and, possibly, also easy to forget).
So, we would have:
def read(stream, parsefunc): try: for source in frozen(parsefunc(stream)): try: record = source.thaw() do_stuff(record) except Exception, e: log_error(e) if not is_recoverable(e): raise recover() except Exception, e: properly_give_up() wrap_up()
(If two try blocks are optional.)
The adapter looks like this:
class Frozen(object): def __init__(self, item): self.value = item def thaw(self): if isinstance(value, Exception): raise value return value def frozen(generator): for item in generator: yield Frozen(item)
And parsefunc looks like this:
def parsefunc(stream): while not eof(stream): try: rec = read_record(stream) do_some_stuff() yield rec except Exception, e: properly_skip_record_or_prepare_retry() yield e
To make it harder to forget the adapter, we could also freeze from function to decorator on parsefunc.
def frozen_results(func): def freezer(__func = func, *args, **kw): for item in __func(*args, **kw): yield Frozen(item) return freezer
In this case, we will declare:
@frozen_results def parsefunc(stream): ...
And we obviously did not bother to declare frozen or wrap it around calling parsefunc .