[Python-ideas] Yield-From: Finalization guarantees

Jacob Holm jh at improva.dk
Tue Mar 31 19:41:16 CEST 2009


Nick Coghlan wrote:
> 4, 3, 2, 1 is the position I've come around to. 
>
> [...snip...]
>
> By adopting position 4, I believe the guarantees for the exception
> handling in the new expression become as simple as possible:
>  - if the subiterator does not provide a throw() method, or the
> exception thrown in is GeneratorExit, then the subiterator's close()
> method (if any) is called and the thrown in exception raised in the
> current frame
>  - otherwise, the exception (including traceback) is passed down to the
> subiterator's throw() method
>   

Below I have attached a heavily annotated version of the expansion that 
I expect for #4.  This version fixes an issue I have forgotten to 
mention where the subiterator is not closed due to an AttributeError 
caused by a missing send method.

> With these semantics, subiterators will be finalised promptly when the
> outermost generator is finalised without any special effort on the
> developer's part and it won't be trivially easy to accidentally suppress
> GeneratorExit.
>   

The way I see it, it will actually be hard to do even on purpose, unless 
you are willing to take a significant performance hit by using a 
non-generator wrapper for every generator.

> To my mind, the practical benefits of such an approach are enough to
> justify the deviation from the general 'inline behaviour' guideline.
>
>   

I disagree, but it seems like I am the only one here that does.   It 
will eliminate a potential pitfall, but will also remove some behavior 
that could have been useful, such as the ability to suppress the 
GeneratorExit if you know what you are doing.

- Jacob


------------------------------------------------------------------------

_i = iter(EXPR)  # Raises TypeError if not an iterable.
try:
    _x = None   # No current exception.
    _y = _i.__next__() # Guaranteed to be there by iter().
    while 1:
        try:
            _s = yield _y
        except BaseException as _e:
            # An exception was thrown in, either by a call to throw() on the generator or implicitly by a call
            # to close().
            _x = _e # Save the thrown-in exception as current.
            if isinstance(_x, GeneratorExit): 
                _m = None  # Don't forward GeneratorExit.
            else:
                _m = getattr(_i, 'throw', None) # Forward any other exception if there is a throw() method.
            if _m is None:
                # Not forwarding. Exit loop and go to finally clause (possibly via "except StopIteration"),
                # which will close _i before reraising _x.
                raise
            _y = _m(_x)
        else:
            if _s is None:
                # Either a send(None) or a __next__(), forward as __next__().
                _x = None # No current exception
                _y = _i.__next__() # Guaranteed to be there by iter().
            else:
                # A send(non-None).  We need to handle the case where the subiterator has no send() method.
                try:
                    _m = _i.send
                except AttributeError as _e:
                    # No send method.  Ensure that the subiterator is closed, then reraise the AttributeError.
                    _x = _e   # Save the AttributeError as the current exception.
                    _m = None # Clear _m so we know _x has not been forwarded.
                    raise     # Exit loop and go to finally clause, which will close _i before reraising _x.
                else:
                    _x = None # No current exception.
                    _y = _m(s)
except StopIteration as _e:
    if _e is _x:
        # If _e was just thrown in, reraise it.  If the exception has been forwarded to the subiterator,
        # the subiterator is assumed closed.  In that case _m will be non-None, so the subiterator will not be
        # closed again by the finally clause.  Conversely, if the exception was not forwarded _m will be None
        # and the finally clause takes care of closing it before reraising the exception.
        raise
    # Normal return.  If we get here, the StopIteration was raised by a __next__(), send() or throw() on the
    # subiterator which will therefore already be closed.  In this case either _x is None or _m is not None, so
    # the the subiterator will not be closed again by the finally clause.
    RESULT = _e.value
finally:
    if _x is not None and _m is None:
        # An exception is active and was not raised by the subiterator.  Explicitly call close before the 
        # exception is automatically reraised by the finally clause.  If close raises an exception, that will
        # take over.
        _m = getattr(_i, 'close', None)
        if _m is not None:
            _m()






More information about the Python-ideas mailing list