I l@ve RuBoard Previous Section Next Section

7.5 Odds and Ends

7.5.1 Passing Optional Data

As we've seen, raise statements can pass an extra data item along with the exception for use in a handler. In general, the extra data allows you to send context information to a handler. In fact, every exception has the extra data; much like function results, it's the special None object if nothing was passed explicitly. The following code illustrates:

myException = 'Error'              # string object

def raiser1():
    raise myException, "hello"     # raise, pass data

def raiser2():
    raise myException              # raise, None implied

def tryer(func):
    except myException, extraInfo:  # run func, catch exception + data
        print 'got this:', extraInfo

% python
>>> from helloexc import *
>>> tryer(raiser1)                  # gets explicitly passed extra data
got this: hello
>>> tryer(raiser2)              # gets None by default
got this: None

7.5.2 The assert Statement

As a special case, Python 1.5 introduced an assert statement, which is mostly syntactic shorthand for a raise. A statement of the form:

assert <test>, <data>          # the <data> part is optional

works like the following code:

if __debug__:
    if not <test>:
        raise AssertionError, <data>

but assert statements may be removed from the compiled program's byte code if the -O command-line flag is used, thereby optimizing the program. Assertion-Error is a built-in exception, and the _ _debug__ flag is a built-in name which is automatically set to 1 unless the -O flag is used. Assertions are typically used to verify program conditions during development; when displayed, their message text includes source-code line information automatically.

7.5.3 Class Exceptions

Recently, Python generalized the notion of exceptions. They may now also be identified by classes and class instances. Like module packages and private class attributes, class exceptions are an advanced topic you can choose to use or not. If you're just getting started, you may want to mark this section as optional reading.

So far we've used strings to identify our own exceptions; when raised, Python matches the exception to except clauses based on object identity (i.e., using the is test we saw in Chapter 2). But when a class exception is raised, an except clause matches the current exception if it names the raised class or a superclass of it. The upshot is that class exceptions support the construction of exception hierarchies: by naming a general exception superclass, an except clause can catch an entire category of exceptions; any more specific subclass will match.

In general, user-defined exceptions may be identified by string or class objects. Beginning with Python 1.5, all built-in exceptions Python may raise are predefined class objects, instead of strings. You normally won't need to care, unless you assume some built-in exception is a string and try to concatenate it without converting (e.g., KeyError + "spam", versus str(KeyError) + "spam"). General raise forms

With the addition of class-based exceptions, the raise statement can take the following five forms: the first two raise string exceptions, the next two raise class exceptions, and the last is an addition in Python Version 1.5, which simply reraises the current exception (it's useful if you need to propagate an arbitrary exception you've caught in a except block). Raising an instance really raises the instance's class; the instance is passed along with the class as the extra data item (it's a good place to store information for the handler).

raise string          # matches except with same string object
raise string, data    # optional extra data (default=None)

raise class, instance # matches except with this class, or a superclass of it
raise instance        # same as: raise instance.__class__, instance

raise                 # re-raise the current exception (new in 1.5)

For backward compatibility with Python versions in which built-in exceptions were strings, you can also use these forms of the raise statement:

raise class                     # same as: raise class()
raise class, arg                # all are really: raise instance
raise class, (arg, arg,...)

These are all the same as saying raise class(arg...), and therefore the same as the raise instance form above (Python calls the class to create and raise an instance of it). For example, you may raise an instance of the built-in KeyError exception by saying simply raise KeyError, even though KeyError is now a class.

If that sounds confusing, just remember that exceptions may be identified by string, class, or class instance objects, and you may pass extra data with the exception or not. If the extra data you pass with a class isn't an instance object, Python makes an instance for you. Example

Let's look at an example to see how class exceptions work. In the following, we define a superclass General and one subclass of it called Specific. We're trying to illustrate the notion of exception categories here; handlers that catch General will also catch a subclass of it like Specific. We then create functions that raise instances of both classes as exceptions and a top-level try that catches General; the same try catches General and Specific exceptions, because Specific is a subclass of General:

class General:           pass
class Specific(General): pass

def raiser1():
    X = General()         # raise listed class instance
    raise X

def raiser2():
    X = Specific()        # raise instance of subclass
    raise X

for func in (raiser1, raiser2):
    except General:               # match General or any subclass of it
        import sys
        print 'caught:', sys.exc_type

% python classexc.py
caught: <class General at 881ee0>
caught: <class Specific at 881100>

Since there are only two possible exceptions here, this doesn't really do justice to the utility of class exceptions; we can achieve the same effects by coding a list of string exception names in the except (e.g., except (a, b, c):), and passing along an instance object as the extra data item. But for large or high exception hierarchies, it may be easier to catch categories using classes than to list every member of a category in a single except clause. Moreover, exception hierarchies can be extended by adding new subclasses, without breaking existing code.

For example, the built-in exception ArithmeticError is a superclass to more specific exceptions such as OverflowError and ZeroDivisionError, but catching just ArithmeticError in a try, you catch any more specific kind of numeric error subclass raised. Furthermore, if you add new kinds of numeric error subclasses in the future, existing code that catches the ArithmeticError superclass (category) also catches the new specific subclasses without modification; there's no need to explicitly extend a list of exception names.

Besides supporting hierarchies, class exceptions also provide storage for extra state information (as instance attributes), but this isn't much more convenient than passing compound objects as extra data with string exceptions (e.g., raise string, object). As usual in Python, the choice to use OOP or not is mostly yours to make.

I l@ve RuBoard Previous Section Next Section