I found myself needing a contextual behavior inside the loop, for instance, a for loop going over a generator, which upon encountering an exception (anything but StopIteration), logs it and continues to the next item.
The problem is, that generators’ implementation doesn’t support any contextual behavior apart from __enter__ and __exit__ which only apply for the loop as a whole, and not for each iteration by itself.
I wonder how broad is this need, whether or not you folks think this is a problem worth addressing, and finally, if theres any simple solution i’m missing?
all the best, be gentle with me it’s my first ever post here
This isn’t really to do with loops, but generators themselves: if a
generator raises an exception, it terminates, like any other function.
So there’s no “resume” for the for-loop to continue with; the
generator’s gone!
Possibly the simplest solution is that the generator doesn’t raise an
exception but yields it inline with whatever other values it in
producing:
def gen():
for ch in 'abc':
try:
assert ch != 'b'
except AssertionError as e:
yield e
else:
yield ch
This would yield:
a
AssertionError # an AssertionError instance
c
Using it:
chars = []
for ch in gen():
if isinstance(ch, Exception):
warning("got an exception: %s", ch)
else:
chars.append(ch)
of course this solves this specific instance of the question, but not nearly covers the scope.
I think a more generic approach is to use a contextmanager inside the loop, wrapping the indented block, but what I was suggesting is to solve it in the generator, in a specifically dedicated functions resembling the enter and exit mechanisms but in finer granularity
Of course, in the for loop itself i expect no modifications, however, inside the generator class I would expect a function looking something like this:
class Generator():
def next():
...
def __enter_iteration__(self, item):
# Here will be a hook for each running immediately after the "next" function invocation
def __exit_iteration__(self, item):
# Here will be a hook for each running at the end of the current iteration
Isn’t this all logic that can be put in __next__ now, without any changes to the iterator protocol?
def __next__(self):
# Stuff from __enter_iteration__
...
rv = ...
# Stuff from __exit_iteration__
return rv
or
# No loop necessary; the try statement wraps the yield statement no matter
# where it occurs.
def generator_function():
try:
# __enter_iteration__
...
yield rv
finally:
# __exit_iteration__
In the worst case scenario, you just don’t use a for loop (which “hides” the call to next that raises the exception), but use a while loop to iterate.
# Equivalent of
#
# for v in your_iterable:
# ...
itr = iter(your_iterable) # Exception unlikely, but you can put this in a try statement
while True:
...
try:
v = next(itr)
except StopIteration:
break
except ...:
...
This is all pretty broad; if there’s a use case you have in mind that doesn’t fit well into any of these three examples, I’d be interested to see it.