[Python-ideas] Change how Generator Expressions handle StopIteration
Steven D'Aprano
steve at pearwood.info
Thu Nov 6 11:15:26 CET 2014
On Thu, Nov 06, 2014 at 07:47:09AM +1000, Nick Coghlan wrote:
> And having said that... what if we introduced UnexpectedStopIteration but
> initially made it a subclass of StopIteration?
>
> We could issue a deprecation warning whenever we triggered the
> StopIteration -> UnexpectedStopIteration conversion, pointing out that at
> some point in the future (3.6? 3.7?), UnexpectedStopIteration will no
> longer be a subclass of StopIteration (perhaps becoming a subclass of
> RuntimeError instead?).
I'm sorry, I have been trying to follow this thread, but there have
been too many wrong turns and side-tracks for me to keep it straight.
What is the problem this is supposed to solve?
Is it just that list (and set and dict) comprehensions treat
StopIteration differently than generator expressions? That is, that
[expr for x in iterator]
list(expr for x in iterator)
are not exactly equivalent, if expr raises StopIteration.
If so, it seems to me that you're adding a lot of conceptual baggage and
complication for very little benefit, and this will probably confuse
people far more than the current situation does. The different treatment
of StopIteration in generator expressions and list comprehensions does
not seem to be a problem for people in practice, judging by the
python-list and tutor mailing lists.
The current situation is simple to learn and understand:
(1) Generator expressions *emit* StopIteration when they are done:
py> next(iter([]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
(2) Functions such as tuple, list, set, dict *absorb* StopIteration:
py> list(iter([]))
[]
py> it = iter([])
py> list(next(it) for y in range(1000))
[]
For-loops do the same, if StopIteration is raised in the "for x in
iterable" header. That's how it knows the loop is done. The "for" part
of a comprehension is the same.
(3) But raising StopIteration in the expression part (or if part) of a
comprehension does not absord the exception, it is treated like any
other exception:
py> [next(iter([])) for y in range(1000)]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
StopIteration
If that is surprising to anyone, I suggest it is because they haven't
considered what happens when you raise StopIteration in the body of a
for-loop:
py> for y in range(1000):
... next(iter([]))
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
StopIteration
To me, the status quo is consistent, understandable, and predictable.
In contrast, you have:
- a solution to something which I'm not sure is even a problem
that needs solving;
- but if it does, the solution seems quite magical, complicated,
and hard to understand;
- it is unclear (to me) under what circumstances StopIteration
will be automatically converted to UnexpectedStopIteration;
- and it seems to me that it will lead to surprising behaviour
when people deliberately raise StopIteration only to have it
mysteriously turn into a different exception, but only
sometimes.
It seems to me that if the difference between comprehensions and
generator expressions really is a problem that needs solving, that the
best way to proceed is using the __future__ mechanism. 3.5 could
introduce
from __future__ comprehensions_absorb_stopiteration
and then 3.6 or 3.7 could make it the default behaviour.
We're still breaking backwards compatibility, but at least we're doing
it cleanly, without magic (well, except the __future__ magic, but that's
well-known and acceptible magic). There will be a transition period
during which people can choose to keep the old behaviour or the new, and
then we transition to the new behaviour. This automatic transformation
of some StopIterations into something else seems like it will be worse
than the problem it is trying to fix.
For what it is worth, I'm a strong -1 on changing the behaviour of
comprehensions at all, but if we must change it in a backwards
incompatible way, +1 on __future__ and -1 on changing the exceptions to
a different exception.
--
Steven
More information about the Python-ideas
mailing list