Re: ???: telling tracebacks to stop - reraising exceptions proposal

Jim Roskind (jar@infoseek.com)
Sat, 26 Mar 1994 00:47:56 +0800

> Date: Sat, 26 Mar 1994 00:11:24 -0500
> From: "Steven D. Majewski" <sdm7g@elvis.med.virginia.edu>
>
> In the past, I have registered some complaints that Python's
> exceptions are sometimes too coarse grained - that you often
> have to catch and exception and check the value parameter to
> see if it's THE exception you are actually expecting to handle.
>
> This would not be a problem if you could just re-raise the
> exception [without destroying the traceback info]

I'm *very* new to Python, so please excuse me if I bring up something
interesting that I just discovered (that maybe everyone else out there
knows about). I figured out how to effectively re-raise an exception,
without destroying the stack trace. It is a bit kludgey, and it
doesn't necessarily service Steven's request, but ... for a slight
semantic change... it would :-).

I needed a way to reraise an exception because I needed to do some
"cleanup" when *any* exception unwound through my stack frame. More
significantly, I *didn't* want the cleanup done when no exception
appeared ('cause everything went as planned!). Alas, it was *very*
difficult to debug problems when I just did:

raise sys.exc_type, sys.exc_value

because, as Steve pointed out, the back trace gets trashed. (I also
thought about using destructors for local variables, as I would in
C++, but there appeared to be fewer guarantees about destruction
time).

The somewhat kludgey solution to my problem (remember, I'm a novice at
Python) is to use a logical variable in the "try: ... finally: ..."
statement.

problem = 1 # presume an exception will take place
try:
func()
problem = 0 # change state to indicate no exception
finally:
if problem:
do_cleanup()

Note that the cleanup operation is done as the exception unwinds past,
and yet I have *not* damaged the backtrace. With this technique in
hand, and the fact that Python has very late binding, I realized that
my "do_cleanup()" function could actually *decide* if I wanted to
catch the exception, or let it go by. In some sense, the ability to
pause, and decide if you wish to catch an exception that has *already*
happened, or pass it on, is totally equivalent to catching it, and
then deciding if you want to reraise it.

The (even more kludgey) method of deciding about catching or passing on
the exception comes from putting a "try: ... except: ..." wrapper
around the above construct. The critical point is that the specific
type of exception to be caught can be decided during the "if
problem:..." statement. The significant fragment looks like this:

try:
catch_type = "" # assume we always re-raise
problem = 1
try:
func()
problem = 0
finally:
if problem:
catch_type = wanna_catch() # decision func
except catch_type:
print "I picked it up locally"

Note that "catch_type" is set during the "finally:..." clause, and
before the "except catch_type" is evaluated. I had to check the
semantics to be sure that the except clause types were not prematurely
calculated, and the above actually works :-).

There is one small gotcha however... sys.exc_type is only set *when*
an except clause is activated (it says it in the manual, and Python
really waits till such a point). As a result, *when* the "finally:"
clause is executed, the value of sys.exc_type has not yet been set,
even though the exception has taken place. If *only* there were a
way to examine the *current* exception prior to reaching an "except:"
clause, then I could really decide (using any criteria) if I wanted to
catch *any* or all exceptions. Assuming we hed equal access to the
current exception value, Steve would have a solution to his problem.

On the positive side, if the programmer can reach a decision about
catching vs re-raising an exception with having to be told what the
exception value is (presumably the decision would be base on some
external state), then the technique works perfectly (be it ever so
kludgey). Hence there is some chance that this *does* solve Steve's
problem.

The following program demonstrates this technique. The function
"func()" randomly generates one of two different exceptions. The
function "wanna_catch()" randomly decided to catch or not to catch an
exception. The bad news is that careful examination of the output
produced shows that sys.exc_type is indeed one-step-behind in its
value. *IF* the semantics changed (i.e., set sys.exc_type ASAP, or
even just prior to a "finally:" clause), or *IF* another var was
provided that was set by the time a finally clause was executed, then
we would have the re-raise functionality.

Late binding is indeed interesting.

Again, since I'm new at Python, and haven't followed this reflector
for a long time, ... SIAK.

Jim Roskind
Author of a YACCable C++ Grammar
408-982-4469
jar@infoseek.com

cut here----------------------------------------------------------
import sys
import rand

def func():
if rand.choice((0,1)):
print " Generating index error"
a = []
a[1]
else:
print " Generating name error"
nonexistent_var_name

def test():
try:
catch_type = "" # assume we catch nothing
problem = 1 # presume an exception will take place
print "About to try func"
try:
func()
problem = 0 # change state to indicate no exception
finally:
if problem:
catch_type = wanna_catch()
except catch_type:
print "I picked it up locally"

# The following simulates a decision function: to catch or not to catch
def wanna_catch():
print " Evaluating sys.exc_type='" + sys.exc_type
catch_type = rand.choice(("", sys.exc_type))
if catch_type:
print " Decided to catch:", catch_type
else:
print " I decided we don't want this one"
return catch_type

def main():
for i in range(20):
try:
test()
except:
print sys.exc_type, "Exception was caught in main"
print

main()