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

Alan Kennedy alanmk at hotmail.com
Thu Jun 5 10:01:55 EDT 2003


[Alan Kennedy]

>> After thinking about this some more, I've come to the conclusion
that explicitly
>> raising "Finality" is the best solution to the try..yield..finally
problem, like
>> so:
>>
>> #--------------------------------
>>
>> class Finality(Exception): pass
>>
>> try:
>>     yield somefunc()
>>     raise Finality
>> except Finality:
>>     print "And finally"
>> except:
>>     print "An exception occurred"
>>
>> #--------------------------------

[Andrew Bennetts]

> A much shorter way to write this is:
>
>     try:
>         yield somefunc()
>         print "And finally"
>     except:
>         print "An exception occurred"

D'oh! Of course it is, thanks Andrew.

I knew I should have just replied to the original thread, and thus
maintained its context, rather than creating a new thread :-|

The problem is when "finally" processing is required, regardless of
whether there is an exception or not. I should have also provided two
different versions of somefunc for which the code would be expected to
work, one which raises an exception and one which does not.

def somefunc():
    return 1/1

and

def somefunc():
    return 1/0

In the case where an exception is raised by somefunc(), the suggested
improvement (to my unclear statement of the problem) does not work. It
does operate correctly when there is no exception, i.e.

def something():
    return 1/1

def finfunc():
    try:
        try:
            # Risky code, e.g.
            yield something()
            print "Finally"
        except:
            print "Got an exception"
    except:
        print "Outer exception handling"

for state in finfunc():
    pass

And produces the same results as how the code would be rewritten using
a Finality exception:-

def something():
    return 1/1

class Finality(Exception): pass

def finfunc():
    try:
        try:
            # Risky code, e.g.
            yield something()
            raise Finality
        except Exception, x:
            print "Finally"
            raise x
    except Finality:
        pass
    except:
        print "Outer exception handling"

for state in finfunc():
    pass

but does not produce the same results when something() generates an
exception, e.g.

def something():
    return 1/0

Now, in the Finality based one, the "finally" code gets executed, but
in the former code, it doesn't. But that's my fault: I misstated the
problem.

> But a better idiom (and still shorter) is:
>
>     try:
>         yield somefunc()
>     except:
>         print "An exception occurred"
>     else:
>         print "And finally"

Also true. But also not related to the problem I should have stated,
which is how to structure generator code so that the cleanup code
always gets executed. In this case, the "else" code is only executed
when there is no exception.

> It also doesn't have the same meaning as a finally block, which is
*always*
> executed upon leaving its corresponding try block, regardless of the
> presence (or absence) of an exception.

As I should have properly stated the problem.

> A more accurate substitute would be:
>
>     exc = None
>     try:
>         yield somefunc()
>     except:
>         exc = sys.exc_info()
>
>     print "finally"
>     if exc:
>         raise exc[0], exc[1], exc[2]

A very interesting alternative.

> But notice that a yield itself can't raise an exception, so you
could in
> fact do:
>
>     try:
>         x = somefunc()
>     finally:
>         print "finally"
>
>     yield x

Again, a good alternative.

But which doesn't address the problem of multiple yields inside
something like a semi-coroutine. For example, how could one code the
original posters problem, which was how to do something like this:

try:
   sock = socket(AF_INET, SOCK_STREAM);
   try:
      sock.setblocking(0); yield stage.done(0)
      try:
         sock.connect((self.host, self.port)); yield stage.done(1)
         yield newComponent([self.setupCSA(sock)])
         while self.waitCSAClose():
            yield stage.current(2)
         yield stage.done(2)
      finally:
         result = s.shutdown(1) ; yield stage.done(3)
   finally:
      sock.close() ; yield stage.done(4)
except error, e:
   # Would handle error more interestingly here
   print stage.lastdone, e

#---------------------

I think that putting the risky code into separate functions, and
saving the results in temporary variables for later 'yield'ing would
be cumbersome, and may not work at all in some situations. And would
also do away with the benefits of using a generator, because it
introduces a standard function call for every yield statement.

Starting again ;-)

I think the core of the problem I was trying to address is: that a
simple and clear method is required for implementing finally style
processing using generators, IFF one is prepared to *guarantee* that
the generator will be sufficiently resumed.

So, restating the proposed solution, with fully specified code

#------------------------------

import random

class Finality(Exception): pass

def finfunc():
    try:
        try:
            # Risky code, e.g.
            for i in range(10):
                x = 1/int(random.random()*10)
                yield x
            print "Successful processing"
            raise Finality
        except Exception, x:
            print "Finally"
            raise x
    except Finality:
        pass
    except Exception, x:
        print "Told you that code was risky, didn't I?: %s" % str(x)

for v in finfunc():
    print v

#------------------------------

I believe this to be the optimal^H^H^H^H^H^H^H a good solution to the
problem of achieving finally-style processing, combined with exception
processing, in a way that is familiar and easy-to-use, i.e. it
operates the same as a try..finally clause, IFF the user guarantees
that the generator will be resumed. The reasons why I think it is a
good solution are

1. The "finally" keyword is not used, therefore no promises or
contracts are broken.

2. The user is *explicitly* made responsible for ensuring that their
generator is resumed as far as the "raise Finality" statement. We all
know that explicit is better than implicit. If the user does not
resume into the generator and thus fails to explicitly "raise Finality",
then their "finally" clauses simply don't get executed.

3. It achieves the intended purpose clearly and concisely, and doesn't
involve any changes to the language syntax or semantics.

I am humbly sorry for any confusion, Andrew. I should have replied to
the original thread, rather than create a new one.

regards,

--
alan kennedy
-----------------------------------------------------
check http headers here: http://xhaus.com/headers
email alan:              http://xhaus.com/mailto/alan




More information about the Python-list mailing list