return from a generator [was:PEP 380 (yield from a subgenerator) comments]
On Thu, Mar 26, 2009 at 4:19 PM, P.J. Eby wrote:
What I don't like is the confusion of adding "return values" to generators, at least using the 'return' statement.
At Fri Mar 27 04:39:48 CET 2009, Guido van Rossum replied:
I'm +1 on "yield from" and +0 on return values in generators.
def g(): yield 42 return 43
for x in g(): print x # probably expected to print 42 and then 43
I still don't see why it needs to be a return statement. Why not make the intent of g explicit, by writing either def g(): yield 42 yield 43 or def g(): yield 42 raise StopIteration(43) -jJ
Jim Jewett wrote:
I still don't see why it needs to be a return statement. Why not make the intent of g explicit
def g(): yield 42 raise StopIteration(43)
Because it would be tedious and ugly, and would actually make the intent *less* clear in the intended use cases. When writing a suspendable function, you're not thinking about iteration. You're thinking about writing a function that performs some computation and possibly returns a value when finished. Have you seen my multithreaded server example? Do you really think the intent of the code would be any clearer if all the returns were replaced by raising StopIteration? -- Greg
def g(): yield 42 return 43
I don't see how the allowing return values in generators can fail to be confusing for people new to Python. Of course, there are limits to the degree that we should let people who are bad at Python dictate our code practices, but I really just feel like this is going to confuse people endlessly. "Where did my 43 go? Why didn't it come up in my loop?" On the other hand, raising is a pretty well understood way of doing control flow, if not value passing. But I can see why "raise StopIteration(42)" might seem a bit obtuse. What if we just subclass StopIteration and make something called "ReturnValue" or some-such. Would a simple rename fix part of the problem: def g(): yield 42 raise ReturnValue(43)
Do you really think the intent of the code would be any clearer if all the returns were replaced by raising StopIteration?
Doesn't "ReturnValue(43)" pretty clearly indicate what's going on? I think part of the appeal of using "return" is that return is what's used in ordinary functions, but if you think about it, you already have to make your cooperative multitasking mini-thread different from an ordinary function anyway by sprinkling yields throughout it. If you're going that far, is it really so bad to also change the "return" to a "raise ReturnValue"? -- Carl Johnson
Carl Johnson wrote:
I think part of the appeal of using "return" is that return is what's used in ordinary functions, but if you think about it, you already have to make your cooperative multitasking mini-thread different from an ordinary function anyway by sprinkling yields throughout it. If you're going that far, is it really so bad to also change the "return" to a "raise ReturnValue"?
The big advantage of "return" over "raise" is that from the point of view of the current execution frame, "return" is a *non-exceptional* exit. This means that: 1. "except" clauses do not execute, but neither do "else" clauses 2. "with" statements pass the appropriate arguments to __exit__ methods to indicate that the frame exit is non-exceptional If you try to use "raise" instead of "return": 1. All except clauses that match the raised exception will execute 2. "with" statements will pass the raised exception into their __exit__ methods Consider a database access subgenerator that writes sent values to the database inside a transaction and then returns the number of rows processed (a *really* rough example just to illustrate the point): def process_rows(db, update_cmd): rows_processed = 0 with db.transaction() as cursor: while 1: values = yield if not values: return rows_processed cursor.run(update_cmd, values) rows_processed += 1 That works just fine using return - the changes will be committed correctly to the database, since this is a non-exceptional exit from the transaction's point of view. If the result must be returned by raising an exception instead, then you not only have to change the return statement into a raise statement, but you have to make sure that it is moved outside the with statement in order to avoid incorrectly rolling back the database transaction. That said, there a couple of fairly straightforward mechanisms I can think of that allow return statements to be used naturally in generators, while still picking up cases that are almost certainly errors. If we don't want to modify existing features (e.g. for loops or contextlib.contextmanager), then we could add a *peer* exception to StopIteration called GeneratorReturn. Existing code which only catches StopIteration would allow the new exception to escape, and the name of that exception could then easily be Googled or looked up in the documentation. The only slight oddity is that a bare return in a generator would still trigger StopIteration, while a "return None" would probably trigger GeneratorReturn(None). Another alternative that avoids the need for a new exception type is to just modify the for loop handling code in the eval loop to check for a non-None StopIteration.value and raise RuntimeError if that occurs. (e.g. RuntimeError("Value set on StopIteration: iterator attempted to return %r to for loop")). contextlib.contextmanager could easily do something similar (it already raises RuntimeError whenever the underlying generator doesn't behave as expected). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia ---------------------------------------------------------------
participants (4)
-
Carl Johnson
-
Greg Ewing
-
Jim Jewett
-
Nick Coghlan