For context, I’ve read PEP 234, and a few questions here and on StackOverflow which are variants on this theme, but…
If calling next(iterable) raises an exception, is the iterator now exhausted? It clearly is if StopIteration (or StopAsyncIteration) is raised, but what about exceptions more generally?
I guess the question has two parts:
- Is it legal for an iterator that raises not to be exhausted?
- Is it pythonic for it not to be exhausted?
Obviously a generator will inevitably become exhausted if it raises, but not every iterator is a generator.
I looked at itertools to see how it behaves in the face of iterators that raise:
import itertools
def collect(iterator):
result = []
while True:
try:
for item in iterator:
result.append(item)
else:
return result
except Exception as e:
result.append(e)
class RaisingIterator:
def __init__(self):
self.i = 0
def __iter__(self):
return self
def __next__(self):
try:
if self.i == 5 or self.i == 15:
raise RuntimeError
elif self.i >= 20:
raise StopIteration
else:
return self.i
finally:
self.i += 1
iterator1 = RaisingIterator()
grouped = itertools.groupby(iterator1, key = lambda i: i//10)
for k,g in grouped:
print(f'group {k=!r}: g->{collect(g)!r}')
# group k=0: g->[0, 1, 2, 3, 4, RuntimeError(), 6, 7, 8, 9]
# group k=1: g->[10, 11, 12, 13, 14, RuntimeError(), 16, 17, 18, 19]
iterator2 = RaisingIterator()
for _ in range(4):
s = itertools.islice(iterator2, 12)
print(f'islice: s->{collect(s)!r}')
# islice: s->[0, 1, 2, 3, 4, RuntimeError()]
# islice: s->[6, 7, 8, 9, 10, 11, 12, 13, 14, RuntimeError()]
# islice: s->[16, 17, 18, 19]
# islice: s->[]
So if one uses groupby, the groups it generates are not exhausted by the source iterator raising an exception, but if one uses islice the slices are.
I’m writing some utilities that manipulate (asynchronous, as it happens) iterators, and how to handle an iterator throwing is a thorn in my side. Should I just relax and say any behaviour is reasonable provided I neither crash nor silently ignore the exception?