Re: Try..Except..Else

Steven D. Majewski (sdm7g@elvis.med.virginia.edu)
Tue, 14 Dec 1993 20:11:44 -0500

[ Please forgive me for posting some disordered thoughts, but
that discussion raises some more general issues that I want
to mention, but I don't quite have time to address in an
orderly way, as they are not just Python problems, but more
general in scope. ]

Re: masking of unexpected errors by code that expects to catch
a particular error.

It looks like there are TWO different sorts of exceptions:
I NON-EXCEPTIONAL, or expected, exceptions:
using try: to catch file open failures, eof, etc. or
Tim's trick of catching KeyError to avoid two dictionary lookups
on "if dict.haskey( key ): dict[key] = value" , on the usual
case where haskey is true.
You fully expect that this instance may happen, so it's not
really 'exceptional' , except that it is relatively rare.
But it is not unplanned. It is just that checking in advance
( does the file exist and do I have read|write access to it?)
is relatively expensive, and returning a status gets into the
old multiple-return value problem.

II EXCEPTIONAL, or unexpected, exceptions:
where sometheing unexpected and unplanned has happened and we
want to handle it as gracefully as possible.
You have no idea what went wrong, but things aren't going as
planned and you need to back out safely, and perhaps retry.

This seems to be very similar to a previous thread where I
asserted that there were really TWO very different sorts of
error messages ( and exceptions ) when considering classes
and modules as black-boxes:

(1) Garbage-In or protocol errors: you are misusing this class:
"you are feeding me garbage, please RTFM!"

(2) Unexpected Errors: "Sorry-This should not happen: I have a bug!"

which are both also related to previous assertions that one of
the problems with exceptions is that the exception name-space
is sometimes too fine-grained, and some-times too coarse, and
thus tend to lead to two types of DWIM bugs:

(1) The "I didn't mean *THAT* I/O error!" bug:
try:
process(file)
except IOError:
do_something_else()
( I may only be interested in catching I/O errors on file, but
if there are I/O errors writing to stdout or stderr, then
do_something_else will mask the REAL error messages. )

(2) And when one has to be specific, there is usually no class or
hierarchy of exceptions, so it's easy to miss one:

try:
value = eval( string )
except ( SyntaxError, NameError, KeyError ):
...

Oops! What about AttributeError ?
In practice, it is an art to predict exactly what exception will
be generated with some expressions. ( And if you use an unqualified
'except:' , you increase the likelihood of masking problems. )

[ The following is not an argument to change anyone's python coding
style. I'm just ruminating on the general problem and trying to
list the solution space. I'm thinking out loud IN python, but not
necessarily ABOUT python. ]

re: the old multiple return value problem:
* Typically, C maps return values into a non-overlapping success range
and a failure range. But this can only be used when there is a
clearly bound success range ( positive values == success, zero or
negative values = error ).

* Some languages (Lisp) allow multiple value returns - this is different
than returning a composite object like a list of tuple - but this
is often awkward to use.

* You can always return a tuple of ( value, status ).
def newopen( file, mode ):
try:
f = open( file, mode ):
return ( f, None )
except ( IOError, ), why
return ( None, why )
file, err = newopen( file, mode )
if err: ...

* You can use exceptions: IF a value is returned, then it is a "good"
value from a successful operation, otherwise, an exception is signaled.

* You can return "objects" which have a state ( which could be a state
that represents that they are not properly initialized ). Methods
which act on these objects must be able to do something sensible
when given an object in the wrong state. Closed files are file objects,
but when you try to read or write them, their methods raise exceptions.
Various network socket objects need to be connected, logged-in or
otherwise initialized before all of their methods can be used.
Maybe open of a non-existant file should yield a "non-existant" file
object, and all file objects would support file.exists(), file.[is]writable(),
file.readable(), etc. Or, alternatively:

class File:
def __init__( self, *args ):
self.file = self.err = None
if args : apply( self.open, args )
def open( self, fname, mode ):
try:
self.file = open( fname, mode )
self.err = None
except (IOError,), why:
self.file = None
self.err = why
def __nonzero__( self ): # interprets class instance in a boolean context
if self.err : return 0
else: return 1
def readline( self ):
self.lastline = self.file.readline()
if not self.lastline : self.err = 'Eof on:' + repr(self.file)
return self.lastline
[ etc, ... ]


which allows:

f = File( file, 'r' )
while f: print f.readline()[:-1]
else: print f.err

Note: I intend to use the fact that objects can have a "boolean context"
( where the value of object.__nonzero__() is used, rather than the
object itself ) and a "sequence context" ( where value of __len__ |
__getitem__ | __setitem__ may be used ) to argue further for a
"function context" for objects: i.e. that there can be a default value
for "object()" that is different from "object" - when I get time to
flesh out my last bunch of "premature comments" !

- Steve Majewski (804-982-0831) <sdm7g@Virginia.EDU>
- UVA Department of Molecular Physiology and Biological Physics