On 11/19/2014 11:24 AM, Chris Angelico wrote:
On Thu, Nov 20, 2014 at 3:03 AM, Nick Coghlan ncoghlan-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org wrote:
OTOH, I'm also not sure the status quo is sufficiently problematic to be worth changing. Yes, it's a little weird, but is it *that* much weirder than the unavoidable issues with exceptions thrown in __next__, __getitem__, __getattr__ and other special methods where a particular type of exception is handled directly by the interpreter?
If you write __next__, you write in a "raise StopIteration" when it's done. If you write __getattr__, you write in "raise AttributeError" if the attribute shouldn't exist. Those are sufficiently explicit that it should be reasonably clear that the exception is the key. But when you write a generator, you don't explicitly raise:
def gen(): yield 1 yield 2 yield 3 return 4
The distinction in __next__ is between returning something and raising something. The distinction in a generator is between "yield" and "return".
Which, as I said a week ago, is why there is no need for "raise StopIteration" in a generator function. The doc clearly states the limited intended use of StopIteration. ''' exception StopIteration Raised by built-in function next() and an iterator‘s __next__() method to signal that there are no further items produced by the iterator. ''' StopIteration is exposed so it can be raised in user coded __next__() and caught when using explicit next(). If it was only used for builtins and for loops, it would not need to be visible.
Why should a generator author have to be concerned about one particular exception having magical meaning?
I am not sure of your intent with this rhetorical (?) question.
Imagine this scenario:
def producer(): """Return user input, or raise KeyboardInterrupt""" return input("Enter the next string: ")
The prompt should be "Enter the next string or hit ^C to quit: ".
def consumer(): """Process the user's input""" while True: try: command = producer() except KeyboardInterrupt: break dispatch(command)
Okay, now let's make a mock producer:
strings = ["do stuff","do more stuff","blah blah"] def mock_producer() if strings: return strings.pop(0) raise KeyboardInterrupt
That's how __next__ works, only with a different exception, and I think people would agree that this is NOT a good use of KeyboardInterrupt.
It is avoidable because the return type of producer is limited to strings. Therefore, producer could (and perhaps should) itself catch KeyboardInterrupt and return None, which is intended for such use. Consumer would then be simplified by replacing 3 lines with "if command is None: break".
If you put a few extra layers in between the producer and consumer, you'd be extremely surprised that an unexpected KeyboardInterrupt just quietly terminated a loop. Yet this is exactly what the generator-and-for-loop model creates: a situation in which StopIteration, despite not being seen at either end of the code, now has magical properties. Without the generator, *only* __next__ has this effect, and that's exactly where it's documented to be.
Does that make for more justification? Unexpected exceptions bubbling up is better than unexpected exceptions quietly terminating loops?