try..yield..finally problem: a proposed solution.

Michael Sparks michaels at rd.bbc.co.uk
Fri Jun 6 12:30:45 CEST 2003


Taking your suggestion, you get a cleaned up version which is
pretty elegant:

def finfunc():
    try:
        try:
            # Risky code, e.g.
            for i in xrange(10):
                t=i/random.randint(0,10)
                yield "data",i
            raise Finality
        except:
            print "Finally"
            raise
    except Finality:
        pass

for v in finfunc():
    print v


However, you find that *if* your code is of the form:

# Form 1
    try:
        ...
        yield
        raise Finality
    except:
        raise

and not of the form:

# Form 2
    try:
        ...
        yield
        raise Finality
    except:
        yield  ### You're going to hate this...
        raise  ### Line A

Then you find this works suggestion works fine, for all exceptions - those
deriving from the Exception class, or otherwise. If you use form 2 however
it breaks horribly.

If you use the original (form 3 for convenience):
# Form 3
    try:
        yield
    except Exception,x:
        yield
        raise x

Then this always works... for exceptions deriving from the Exception
class. (and breaks with any legacy code)

What breaks?

If you use form 2, when you yield, the "current exception" is cleared.

ie when you do the raise on Line A you get a 'exceptions must not be of
NoneType' error. Clearly whereever this current exception is stored, it
either isn't in the python stack frame that's put aside when you yield,
or it's explicitly cleared. This strikes me as either a bug, or an
undocumented "boundary feature".

Minimal test case:

def finfunc():
    try:
        yield 1
        raise Exception
    except:
        yield 2
        raise

for v in finfunc():
    print v

However, in Form 3, we catch and bind the exception to x, so that despite
the current exception being cleared we can still rethrow, but of course
you lose the benefits of the stack trace pointing at the right place...

Form 3 however has the disadvantage that because it doesn't catch all
exceptions, it means that you don't have the same try...finally semantics
that have with Form 1. (Some exceptions - even if they're not stylistically
nice - can still get through and cause breakage.)

Having played with this for a few days, and reading the discussion I think
that the decision to disallow:
    try:
        yield
    finally:
        #stuff

Is a good one - it results in too great a likelyhood of people shooting
themselves in the foot.

However the cleanest alternative:
    try:
        yield
    except:
        #stuff
        raise

Suffers from the downside that if you yield inside the except clause that
the current exception is cleared. (Which strikes me as a bug, since you'd
expect the runtime state in the generator not to changed after a yield.
Also I'd view it as a bug since otherwise I can write the code looking
cleaner in C ;)

Form 1 is the nicest from many perspectives, but Form 3 always works. I'm not
sure of the best way of resolving this, or whether to just use my original
non-try-finally versions instead now :-)



Michael
--
Michael.Sparks at rd.bbc.co.uk
British Broadcasting Corporation, Research and Development
Kingswood Warren, Surrey KT20 6NP

This message (and any attachments) may contain personal views
which are not the views of the BBC unless specifically stated.




More information about the Python-list mailing list