Revised**11 PEP on Yield-From

Draft 12 of the PEP. Fixed a bug in the expansion (didn't handle StopIteration raised by throw). Removed paragraph about StopIteration left over from an earlier version. Added some discussion about rejected ideas. -- Greg PEP: XXX Title: Syntax for Delegating to a Subgenerator Version: $Revision$ Last-Modified: $Date$ Author: Gregory Ewing <greg.ewing@canterbury.ac.nz> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 13-Feb-2009 Python-Version: 3.x Post-History: Abstract ======== A syntax is proposed for a generator to delegate part of its operations to another generator. This allows a section of code containing 'yield' to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator. The new syntax also opens up some opportunities for optimisation when one generator re-yields values produced by another. Motivation ========== A Python generator is a form of coroutine, but has the limitation that it can only yield to its immediate caller. This means that a piece of code containing a ``yield`` cannot be factored out and put into a separate function in the same way as other code. Performing such a factoring causes the called function to itself become a generator, and it is necessary to explicitly iterate over this second generator and re-yield any values that it produces. If yielding of values is the only concern, this can be performed without much difficulty using a loop such as :: for v in g: yield v However, if the subgenerator is to interact properly with the caller in the case of calls to ``send()``, ``throw()`` and ``close()``, things become considerably more difficult. As will be seen later, the necessary code is very complicated, and it is tricky to handle all the corner cases correctly. A new syntax will be proposed to address this issue. In the simplest use cases, it will be equivalent to the above for-loop, but it will also handle the full range of generator behaviour, and allow generator code to be refactored in a simple and straightforward way. Proposal ======== The following new expression syntax will be allowed in the body of a generator: :: yield from <expr> where <expr> is an expression evaluating to an iterable, from which an iterator is extracted. The iterator is run to exhaustion, during which time it yields and receives values directly to or from the caller of the generator containing the ``yield from`` expression (the "delegating generator"). Furthermore, when the iterator is another generator, the subgenerator is allowed to execute a ``return`` statement with a value, and that value becomes the value of the ``yield from`` expression. The full semantics of the ``yield from`` expression can be described in terms of the generator protocol as follows: * Any values that the iterator yields are passed directly to the caller. * Any values sent to the delegating generator using ``send()`` are passed directly to the iterator. If the sent value is None, the iterator's ``next()`` method is called. If the sent value is not None, the iterator's ``send()`` method is called. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator. * Exceptions other than GeneratorExit thrown into the delegating generator are passed to the ``throw()`` method of the iterator. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator. * If a GeneratorExit exception is thrown into the delegating generator, or the ``close()`` method of the delegating generator is called, then the ``close()`` method of the iterator is called if it has one. If this call results in an exception, it is propagated to the delegating generator. Otherwise, GeneratorExit is raised in the delegating generator. * The value of the ``yield from`` expression is the first argument to the ``StopIteration`` exception raised by the iterator when it terminates. * ``return expr`` in a generator causes ``StopIteration(expr)`` to be raised. Enhancements to StopIteration ----------------------------- For convenience, the ``StopIteration`` exception will be given a ``value`` attribute that holds its first argument, or None if there are no arguments. Formal Semantics ---------------- Python 3 syntax is used in this section. 1. The statement :: RESULT = yield from EXPR is semantically equivalent to :: _i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit: _m = getattr(_i, 'close', None) if _m is not None: _m() raise except: _m = getattr(_i, 'throw', None) if _m is not None: try: _y = _m(*sys.exc_info()) except StopIteration as _e: _r = _e.value break else: raise else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r 2. In a generator, the statement :: return value is semantically equivalent to :: raise StopIteration(value) except that, as currently, the exception cannot be caught by ``except`` clauses within the returning generator. 3. The StopIteration exception behaves as though defined thusly: :: class StopIteration(Exception): def __init__(self, *args): if len(args) > 0: self.value = args[0] else: self.value = None Exception.__init__(self, *args) Rationale ========= The Refactoring Principle ------------------------- The rationale behind most of the semantics presented above stems from the desire to be able to refactor generator code. It should be possible to take an section of code containing one or more ``yield`` expressions, move it into a separate function (using the usual techniques to deal with references to variables in the surrounding scope, etc.), and call the new function using a ``yield from`` expression. The behaviour of the resulting compound generator should be, as far as reasonably practicable, the same as the original unfactored generator in all situations, including calls to ``next()``, ``send()``, ``throw()`` and ``close()``. The semantics in cases of subiterators other than generators has been chosen as a reasonable generalization of the generator case. The proposed semantics have the following limitations with regard to refactoring: * A block of code that catches GeneratorExit without subsequently re-raising it cannot be factored out while retaining exactly the same behaviour. * Factored code may not behave the same way as unfactored code if a StopIteration exception is thrown into the delegating generator. With use cases for these being rare to non-existent, it was not considered worth the extra complexity required to support them. Finalization ------------ There was some debate as to whether explicitly finalizing the delegating generator by calling its ``close()`` method while it is suspended at a ``yield from`` should also finalize the subiterator. An argument against doing so is that it would result in premature finalization of the subiterator if references to it exist elsewhere. Consideration of non-refcounting Python implementations led to the decision that this explicit finalization should be performed, so that explicitly closing a factored generator has the same effect as doing so to an unfactored one in all Python implementations. The assumption made is that, in the majority of use cases, the subiterator will not be shared. The rare case of a shared subiterator can be accommodated by means of a wrapper that blocks ``throw()`` and ``close()`` calls, or by using a means other than ``yield from`` to call the subiterator. Generators as Threads --------------------- A motivation for generators being able to return values concerns the use of generators to implement lightweight threads. When using generators in that way, it is reasonable to want to spread the computation performed by the lightweight thread over many functions. One would like to be able to call a subgenerator as though it were an ordinary function, passing it parameters and receiving a returned value. Using the proposed syntax, a statement such as :: y = f(x) where f is an ordinary function, can be transformed into a delegation call :: y = yield from g(x) where g is a generator. One can reason about the behaviour of the resulting code by thinking of g as an ordinary function that can be suspended using a ``yield`` statement. When using generators as threads in this way, typically one is not interested in the values being passed in or out of the yields. However, there are use cases for this as well, where the thread is seen as a producer or consumer of items. The ``yield from`` expression allows the logic of the thread to be spread over as many functions as desired, with the production or consumption of items occuring in any subfunction, and the items are automatically routed to or from their ultimate source or destination. Concerning ``throw()`` and ``close()``, it is reasonable to expect that if an exception is thrown into the thread from outside, it should first be raised in the innermost generator where the thread is suspended, and propagate outwards from there; and that if the thread is terminated from outside by calling ``close()``, the chain of active generators should be finalised from the innermost outwards. Syntax ------ The particular syntax proposed has been chosen as suggestive of its meaning, while not introducing any new keywords and clearly standing out as being different from a plain ``yield``. Optimisations ------------- Using a specialised syntax opens up possibilities for optimisation when there is a long chain of generators. Such chains can arise, for instance, when recursively traversing a tree structure. The overhead of passing ``next()`` calls and yielded values down and up the chain can cause what ought to be an O(n) operation to become, in the worst case, O(n\*\*2). A possible strategy is to add a slot to generator objects to hold a generator being delegated to. When a ``next()`` or ``send()`` call is made on the generator, this slot is checked first, and if it is nonempty, the generator that it references is resumed instead. If it raises StopIteration, the slot is cleared and the main generator is resumed. This would reduce the delegation overhead to a chain of C function calls involving no Python code execution. A possible enhancement would be to traverse the whole chain of generators in a loop and directly resume the one at the end, although the handling of StopIteration is more complicated then. Use of StopIteration to return values ------------------------------------- There are a variety of ways that the return value from the generator could be passed back. Some alternatives include storing it as an attribute of the generator-iterator object, or returning it as the value of the ``close()`` call to the subgenerator. However, the proposed mechanism is attractive for a couple of reasons: * Using a generalization of the StopIteration exception makes it easy for other kinds of iterators to participate in the protocol without having to grow an extra attribute or a close() method. * It simplifies the implementation, because the point at which the return value from the subgenerator becomes available is the same point at which the exception is raised. Delaying until any later time would require storing the return value somewhere. Rejected Ideas -------------- Some ideas were discussed but rejected. Suggestion: There should be some way to prevent the initial call to next(), or substitute it with a send() call with a specified value, the intention being to support the use of generators wrapped so that the initial next() is performed automatically. Resolution: Outside the scope of the proposal. Such generators should not be used with ``yield from``. Suggestion: If closing a subiterator raises StopIteration with a value, return that value from the ``close()`` call to the delegating generator. Resolution: Undesirable for a number of reasons. The purpose of closing a generator is to ensure proper cleanup, not to obtain a meaningful return value. Also, it would be unreliable unless the return value were stored so as to be available to subsequent close calls, which would cause it to persist for longer than expected. Suggestion: If ``close()`` is not to return a value, then raise an exception if StopIteration with a non-None value occurs. Resolution: No clear reason to do so. Ignoring a return value is not considered an error anywhere else in Python. Criticisms ========== Under this proposal, the value of a ``yield from`` expression would be derived in a very different way from that of an ordinary ``yield`` expression. This suggests that some other syntax not containing the word ``yield`` might be more appropriate, but no acceptable alternative has so far been proposed. Rejected alternatives include ``call``, ``delegate`` and ``gcall``. It has been suggested that some mechanism other than ``return`` in the subgenerator should be used to establish the value returned by the ``yield from`` expression. However, this would interfere with the goal of being able to think of the subgenerator as a suspendable function, since it would not be able to return values in the same way as other functions. The use of an exception to pass the return value has been criticised as an "abuse of exceptions", without any concrete justification of this claim. In any case, this is only one suggested implementation; another mechanism could be used without losing any essential features of the proposal. Alternative Proposals ===================== Proposals along similar lines have been made before, some using the syntax ``yield *`` instead of ``yield from``. While ``yield *`` is more concise, it could be argued that it looks too similar to an ordinary ``yield`` and the difference might be overlooked when reading code. To the author's knowledge, previous proposals have focused only on yielding values, and thereby suffered from the criticism that the two-line for-loop they replace is not sufficiently tiresome to write to justify a new syntax. By dealing with the full generator protocol, this proposal provides considerably more benefit. Additional Material =================== Some examples of the use of the proposed syntax are available, and also a prototype implementation based on the first optimisation outlined above. `Examples and Implementation`_ .. _Examples and Implementation: http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/ Copyright ========= This document has been placed in the public domain. .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End:

Greg Ewing wrote:
Just so you know, I now agree that a long expansion with multiple "try...except StopIteration" blocks is the right thing to do. There are only two cases I can see where it makes a difference compared to what I suggested: 1. Throwing StopIteration to an iterator without a throw() method. I would prefer treating this case *exactly* as if the iterator had a trivial throw method: "def throw(self, et, ev=None, tb=None): raise et, ev, tb". Meaning that the StopIteration *should* be caught by yield-from. Treating it like this makes it easier to write wrappers that don't accidentally change the semantics of an obscure corner case. Principle of least surprise and all that... It is easy enough to change the expansion to do this by expanding the try block around the throw() call or by actually using such a trivial throw method as a fallback. Alternatively, the expansion can be rewritten using functools.partial as in the bottom of this mail. It has identical semantics to draft 12 of the PEP, except for the handling of the missing throw method. I actually like that version because it is careful about what exceptions to catch, but still only has one "try...except StopIteration". YMMV. 2. Calling an iterator.close() that raises a StopIteration. Arguably, such a close() is an error, so getting an exception in the caller is better than swallowing it and turning it into a normal return. Especially since we only called close() as part of handling GeneratorExit in the first place. An unrelated question... What should happen with an iterator that has a throw or close attribute that just happens to have the value None? Should that be treated as an error because None is not callable, or should it be treated as if the attribute wasn't there? The expansion handles it as if the attribute wasn't there, but IIRC your patch will raise a TypeError trying to call None.
Looks good, except...
I may have been unclear about why I thought this should raise a RuntimeError. As I see it there are only two code patterns in a generator that would have close() catch a StopIteration with a non-None value. * An explicit catch of GeneratorExit followed by "return Value". This is harmless and potentially useful, although probably an abuse of GeneratorExit (that was one of the early arguments for not returning a value from close). Not raising a RuntimeError in close makes it simpler to share a code path between the common and the forced exit. * An implicit catch of GeneratorExit, followed by "return Value". By an "implicit catch", I mean either a catch of "BaseException" or a "finally" clause. In both cases, "return Value" will hide the original exception and that is almost certainly a bug. Raising a RuntimeError would let you discover this bug early. The question now is whether it is better to catch n00b errors or to allow careful programmers a bit more freedom in how they structure their code. When I started writing this mail I was leaning towards catching errors, but I have now changed my mind. I think giving more power to experienced users is more important. Best regards - Jacob ------------------------------------------------------------------------ _i = iter(EXPR) _p = partial(next, _i) while 1: try: _y = _p() except StopIteration as _e: _r = _e.value break try: _s = yield _y except GeneratorExit: _m = getattr(_i, 'close', None) if _m is not None: _m() raise except: _m = getattr(_i, 'throw', None) if _m is None: def _m(et, ev, tb): raise et, ev, tb _p = partial(_m, *sys.exc_info()) else: if _s is None: _p = partial(next, _i) else: _p = partial(_i.send, _s) RESULT = _r

Jacob Holm wrote:
1. Throwing StopIteration to an iterator without a throw() method.
Guido seems happy not to care what happens if you throw StopIteration in, so I'm happy to do so as well -- it saves considerable complication.
There are plenty of other ways to get strange results by raising StopIteration in places where you shouldn't, so I'm not worried about this either.
That's a good point -- I hadn't intended that.
Doing either of those things *anywhere* is likely to hide a bug. I don't see a strong reason to single out this particular case and try to detect it.
I have now changed my mind. I think giving more power to experienced users is more important.
My reasoning is more along the lines that it's not worth the bother of trying to detect this error, even if it's an error at all, which isn't entirely certain. -- Greg

Greg Ewing wrote:
I was about to reiterate how treating a missing throw like I suggested would make my wrappers simpler, but realized that it actually makes very little difference.
Ok. Note that treating None as missing might actually be useful by making it easier to "hide" the throw() or close() method of a base class from yield-from. IIRC there is a precedent for treating None this way in the handling of hash(). Even without handling None this way it is still possible to hide the base class methods by creating a property that raises AttributeError, so this is not all that important. I just think that using None is slightly cleaner for this use. Cheers - Jacob

Looks good. I can almost follow it! IIRC, there were some suggestions given for motivating examples that were posted in the thread, but I don't see any of them either in the PEP itself nor at your URL with examples. -- Aahz (aahz@pythoncraft.com) <*> http://www.pythoncraft.com/ "If you think it's expensive to hire a professional to do the job, wait until you hire an amateur." --Red Adair

Trying again, as the last version was mangled. (Thanks to Aahz for pointing that out). I hope this is better... Greg Ewing wrote:
Just so you know, I now agree that a long expansion with multiple "try...except StopIteration" blocks is the right thing to do. There are only two cases I can see where it makes a difference compared to what I suggested: 1. Throwing StopIteration to an iterator without a throw() method. I would prefer treating this case *exactly* as if the iterator had a trivial throw method: def throw(self, et, ev=None, tb=None): raise et, ev, tb In other words, I think the StopIteration *should* be caught by yield-from. Treating it like this makes it easier to write wrappers that don't accidentally change the semantics of an obscure corner case. Principle of least surprise and all that... It is easy enough to change the expansion to do this by expanding the try block around the throw() call or by actually using such a trivial throw method as a fallback. Alternatively, the expansion can be rewritten using functools.partial as in the bottom of this mail. It has identical semantics to draft 12 of the PEP, except for the handling of the missing throw method. I actually like that version because it is careful about what exceptions to catch, but still only has one "try...except StopIteration". YMMV. 2. Calling an iterator.close() that raises a StopIteration. Arguably, such a close() is an error, so getting an exception in the caller is better than swallowing it and turning it into a normal return. Especially since we only called close() as part of handling GeneratorExit in the first place. An unrelated question... What should happen with an iterator that has a throw or close attribute that just happens to have the value None? Should that be treated as an error because None is not callable, or should it be treated as if the attribute wasn't there? The expansion handles it as if the attribute wasn't there, but IIRC your patch will raise a TypeError trying to call None.
Looks good, except...
I may have been unclear about why I thought this should raise a RuntimeError. As I see it there are only two code patterns in a generator that would have close() catch a StopIteration with a non-None value. * An explicit catch of GeneratorExit followed by "return Value". This is harmless and potentially useful, although probably an abuse of GeneratorExit (that was one of the early arguments for not returning a value from close). Not raising a RuntimeError in close makes it simpler to share a code path between the common and the forced exit. * An implicit catch of GeneratorExit, followed by "return Value". By an "implicit catch", I mean either a catch of "BaseException" or a "finally" clause. In both cases, "return Value" will hide the original exception and that is almost certainly a bug. Raising a RuntimeError would let you discover this bug early. The question now is whether it is better to catch n00b errors or to allow careful programmers a bit more freedom in how they structure their code. When I started writing this mail I was leaning towards catching errors, but I have now changed my mind. I think giving more power to experienced users is more important. Best regards - Jacob ------------------------------------------------------------------------ _i = iter(EXPR) _p = partial(next, _i) while 1: try: _y = _p() except StopIteration as _e: _r = _e.value break try: _s = yield _y except GeneratorExit: _m = getattr(_i, 'close', None) if _m is not None: _m() raise except: _m = getattr(_i, 'throw', None) if _m is None: def _m(et, ev, tb): raise et, ev, tb _p = partial(_m, *sys.exc_info()) else: if _s is None: _p = partial(next, _i) else: _p = partial(_i.send, _s) RESULT = _r

Greg Ewing wrote:
Draft 12 of the PEP....
In the ensuing discussion, I replied to one of Jacob Holm's points by making a comment that was really meant to apply to the PEP itself, rather than his suggestion. I replied at the place where it became obvious to me how I felt the PEP could become clearer. In an off-line exchange, I found he thought I was talking simply about his suggested change, so I'll restate my case here. I do so not for emphasis, but rather to attach it to the proper context. This is, of course, Greg's decision for readability. I share Jacob's fear of bike-shed discussions on variable names (I am not necessarily in love with the names I've chosen here). Now that we passed the magic three or four threshold, is it not easier to read if we pick some better names? Instead of:
we could use: _iterator = iter(EXPR) try: _out = next(_iterator) except StopIteration as _error: _result = _error.value else: while 1: try: _inner = yield _out except GeneratorExit: _close = getattr(_i, 'close', None) if _close is not None: _close() raise except: _throw = getattr(_iterator, 'throw', None) if _throw is not None: try: _out = _throw(*sys.exc_info()) except StopIteration as _error: _result = _error.value break else: raise else: try: if _inner is None: _out = next(_iterator) else: _out = _i.send(_inner) except StopIteration as _error: _result = _error.value break RESULT = _result --Scott David Daniels Scott.Daniels@Acm.Org

Greg Ewing wrote:
Just so you know, I now agree that a long expansion with multiple "try...except StopIteration" blocks is the right thing to do. There are only two cases I can see where it makes a difference compared to what I suggested: 1. Throwing StopIteration to an iterator without a throw() method. I would prefer treating this case *exactly* as if the iterator had a trivial throw method: "def throw(self, et, ev=None, tb=None): raise et, ev, tb". Meaning that the StopIteration *should* be caught by yield-from. Treating it like this makes it easier to write wrappers that don't accidentally change the semantics of an obscure corner case. Principle of least surprise and all that... It is easy enough to change the expansion to do this by expanding the try block around the throw() call or by actually using such a trivial throw method as a fallback. Alternatively, the expansion can be rewritten using functools.partial as in the bottom of this mail. It has identical semantics to draft 12 of the PEP, except for the handling of the missing throw method. I actually like that version because it is careful about what exceptions to catch, but still only has one "try...except StopIteration". YMMV. 2. Calling an iterator.close() that raises a StopIteration. Arguably, such a close() is an error, so getting an exception in the caller is better than swallowing it and turning it into a normal return. Especially since we only called close() as part of handling GeneratorExit in the first place. An unrelated question... What should happen with an iterator that has a throw or close attribute that just happens to have the value None? Should that be treated as an error because None is not callable, or should it be treated as if the attribute wasn't there? The expansion handles it as if the attribute wasn't there, but IIRC your patch will raise a TypeError trying to call None.
Looks good, except...
I may have been unclear about why I thought this should raise a RuntimeError. As I see it there are only two code patterns in a generator that would have close() catch a StopIteration with a non-None value. * An explicit catch of GeneratorExit followed by "return Value". This is harmless and potentially useful, although probably an abuse of GeneratorExit (that was one of the early arguments for not returning a value from close). Not raising a RuntimeError in close makes it simpler to share a code path between the common and the forced exit. * An implicit catch of GeneratorExit, followed by "return Value". By an "implicit catch", I mean either a catch of "BaseException" or a "finally" clause. In both cases, "return Value" will hide the original exception and that is almost certainly a bug. Raising a RuntimeError would let you discover this bug early. The question now is whether it is better to catch n00b errors or to allow careful programmers a bit more freedom in how they structure their code. When I started writing this mail I was leaning towards catching errors, but I have now changed my mind. I think giving more power to experienced users is more important. Best regards - Jacob ------------------------------------------------------------------------ _i = iter(EXPR) _p = partial(next, _i) while 1: try: _y = _p() except StopIteration as _e: _r = _e.value break try: _s = yield _y except GeneratorExit: _m = getattr(_i, 'close', None) if _m is not None: _m() raise except: _m = getattr(_i, 'throw', None) if _m is None: def _m(et, ev, tb): raise et, ev, tb _p = partial(_m, *sys.exc_info()) else: if _s is None: _p = partial(next, _i) else: _p = partial(_i.send, _s) RESULT = _r

Jacob Holm wrote:
1. Throwing StopIteration to an iterator without a throw() method.
Guido seems happy not to care what happens if you throw StopIteration in, so I'm happy to do so as well -- it saves considerable complication.
There are plenty of other ways to get strange results by raising StopIteration in places where you shouldn't, so I'm not worried about this either.
That's a good point -- I hadn't intended that.
Doing either of those things *anywhere* is likely to hide a bug. I don't see a strong reason to single out this particular case and try to detect it.
I have now changed my mind. I think giving more power to experienced users is more important.
My reasoning is more along the lines that it's not worth the bother of trying to detect this error, even if it's an error at all, which isn't entirely certain. -- Greg

Greg Ewing wrote:
I was about to reiterate how treating a missing throw like I suggested would make my wrappers simpler, but realized that it actually makes very little difference.
Ok. Note that treating None as missing might actually be useful by making it easier to "hide" the throw() or close() method of a base class from yield-from. IIRC there is a precedent for treating None this way in the handling of hash(). Even without handling None this way it is still possible to hide the base class methods by creating a property that raises AttributeError, so this is not all that important. I just think that using None is slightly cleaner for this use. Cheers - Jacob

Looks good. I can almost follow it! IIRC, there were some suggestions given for motivating examples that were posted in the thread, but I don't see any of them either in the PEP itself nor at your URL with examples. -- Aahz (aahz@pythoncraft.com) <*> http://www.pythoncraft.com/ "If you think it's expensive to hire a professional to do the job, wait until you hire an amateur." --Red Adair

Trying again, as the last version was mangled. (Thanks to Aahz for pointing that out). I hope this is better... Greg Ewing wrote:
Just so you know, I now agree that a long expansion with multiple "try...except StopIteration" blocks is the right thing to do. There are only two cases I can see where it makes a difference compared to what I suggested: 1. Throwing StopIteration to an iterator without a throw() method. I would prefer treating this case *exactly* as if the iterator had a trivial throw method: def throw(self, et, ev=None, tb=None): raise et, ev, tb In other words, I think the StopIteration *should* be caught by yield-from. Treating it like this makes it easier to write wrappers that don't accidentally change the semantics of an obscure corner case. Principle of least surprise and all that... It is easy enough to change the expansion to do this by expanding the try block around the throw() call or by actually using such a trivial throw method as a fallback. Alternatively, the expansion can be rewritten using functools.partial as in the bottom of this mail. It has identical semantics to draft 12 of the PEP, except for the handling of the missing throw method. I actually like that version because it is careful about what exceptions to catch, but still only has one "try...except StopIteration". YMMV. 2. Calling an iterator.close() that raises a StopIteration. Arguably, such a close() is an error, so getting an exception in the caller is better than swallowing it and turning it into a normal return. Especially since we only called close() as part of handling GeneratorExit in the first place. An unrelated question... What should happen with an iterator that has a throw or close attribute that just happens to have the value None? Should that be treated as an error because None is not callable, or should it be treated as if the attribute wasn't there? The expansion handles it as if the attribute wasn't there, but IIRC your patch will raise a TypeError trying to call None.
Looks good, except...
I may have been unclear about why I thought this should raise a RuntimeError. As I see it there are only two code patterns in a generator that would have close() catch a StopIteration with a non-None value. * An explicit catch of GeneratorExit followed by "return Value". This is harmless and potentially useful, although probably an abuse of GeneratorExit (that was one of the early arguments for not returning a value from close). Not raising a RuntimeError in close makes it simpler to share a code path between the common and the forced exit. * An implicit catch of GeneratorExit, followed by "return Value". By an "implicit catch", I mean either a catch of "BaseException" or a "finally" clause. In both cases, "return Value" will hide the original exception and that is almost certainly a bug. Raising a RuntimeError would let you discover this bug early. The question now is whether it is better to catch n00b errors or to allow careful programmers a bit more freedom in how they structure their code. When I started writing this mail I was leaning towards catching errors, but I have now changed my mind. I think giving more power to experienced users is more important. Best regards - Jacob ------------------------------------------------------------------------ _i = iter(EXPR) _p = partial(next, _i) while 1: try: _y = _p() except StopIteration as _e: _r = _e.value break try: _s = yield _y except GeneratorExit: _m = getattr(_i, 'close', None) if _m is not None: _m() raise except: _m = getattr(_i, 'throw', None) if _m is None: def _m(et, ev, tb): raise et, ev, tb _p = partial(_m, *sys.exc_info()) else: if _s is None: _p = partial(next, _i) else: _p = partial(_i.send, _s) RESULT = _r

Greg Ewing wrote:
Draft 12 of the PEP....
In the ensuing discussion, I replied to one of Jacob Holm's points by making a comment that was really meant to apply to the PEP itself, rather than his suggestion. I replied at the place where it became obvious to me how I felt the PEP could become clearer. In an off-line exchange, I found he thought I was talking simply about his suggested change, so I'll restate my case here. I do so not for emphasis, but rather to attach it to the proper context. This is, of course, Greg's decision for readability. I share Jacob's fear of bike-shed discussions on variable names (I am not necessarily in love with the names I've chosen here). Now that we passed the magic three or four threshold, is it not easier to read if we pick some better names? Instead of:
we could use: _iterator = iter(EXPR) try: _out = next(_iterator) except StopIteration as _error: _result = _error.value else: while 1: try: _inner = yield _out except GeneratorExit: _close = getattr(_i, 'close', None) if _close is not None: _close() raise except: _throw = getattr(_iterator, 'throw', None) if _throw is not None: try: _out = _throw(*sys.exc_info()) except StopIteration as _error: _result = _error.value break else: raise else: try: if _inner is None: _out = next(_iterator) else: _out = _i.send(_inner) except StopIteration as _error: _result = _error.value break RESULT = _result --Scott David Daniels Scott.Daniels@Acm.Org
participants (4)
-
Aahz
-
Greg Ewing
-
Jacob Holm
-
Scott David Daniels