[Python-ideas] Revised**11 PEP on Yield-From

Jacob Holm jh at improva.dk
Fri Apr 17 18:15:32 CEST 2009


Trying again, as the last version was mangled. (Thanks to Aahz for 
pointing that out).  I hope this is better...


Greg Ewing wrote:
 > Draft 12 of the PEP.
 >
 > Fixed a bug in the expansion (didn't handle StopIteration raised by
 > throw).
 >

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.

 > Removed paragraph about StopIteration left over from an earlier
 > version.
 >
 > Added some discussion about rejected ideas.
 >

Looks good, except...

 > 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.
 >

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







More information about the Python-ideas mailing list