DekGenius.com
[ Team LiB ] Previous Section Next Section

25.2 Class-Based Exceptions

Strings are a simple way to define your exceptions. Exceptions may also be identified with classes. Like some other topics we've met in this book, class exceptions are an advanced topic you can choose to use or not in Python 2.2. However, classes have some added value that merits a quick look; in particular, they allow us to identify exception categories that are more flexible to use and maintain than simple strings. Moreover, classes are likely to become the prescribed way to identify your exceptions in the future.

The chief difference between string and class exceptions has to do with the way that exceptions raised are matched against except clauses in try statements:

  • String exceptions are matched by simple object identity: the raised exception is matched to except clauses by Python's is test (not ==).

  • Class exceptions are matched by superclass relationships: the raised exception matches an except clause, if that except clause names the exception's class or any superclass of it.

That is, when a try statement's except clause lists a superclass, it catches instances of that superclass, as well as instances of all its subclasses lower in the class tree. The net effect is that class exceptions support the construction of exception hierarchies: superclasses become category names, and subclasses become specific kinds of exceptions within a category. By naming a general exception superclass, an except clause can catch an entire category of exceptions—any more specific subclass will match.

25.2.1 Class Exception Example

Let's look at an example to see how class exceptions work in code. In the following file, classexc.py, we define a superclass General and two subclasses of it called Specific1 and Specific2. We're illustrating the notion of exception categories here: General is a category name, and its two subclasses are specific types of exceptions within the category. Handlers that catch General will also catch any subclasses of it, including Specific1 and Specific2.

class General:            pass
class Specific1(General): pass
class Specific2(General): pass

def raiser0(  ):
    X = General(  )          # Raise superclass instance.
    raise X

def raiser1(  ):
    X = Specific1(  )        # Raise subclass instance.
    raise X

def raiser2(  ):
    X = Specific2(  )        # Raise different subclass instance.
    raise X

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

C:\python> python classexc.py
caught: __main__.General
caught: __main__.Specific1
caught: __main__.Specific2

Notice that we call classes to make instances in the raise statements here; as we'll see when we formalize raise statement forms later in this section, an instance is always present when raising class-based exceptions. This code also includes functions that raise instances of all three classes as exceptions, and a top-level try that calls the functions and catches General exceptions. The same try catches General and the two specific exceptions, because the two specific exceptions are subclasses of General.

25.2.2 Why Class Exceptions?

Since there are only three possible exceptions in the prior section's example, it doesn't really do justice to the utility of class exceptions. In fact, we can achieve the same effects by coding a list of string exception names in parenthesis within the except clause. File stringexc.py shows how:

General   = 'general'
Specific1 = 'specific1'
Specific2 = 'specific2'

def raiser0(  ): raise General
def raiser1(  ): raise Specific1
def raiser2(  ): raise Specific2

for func in (raiser0, raiser1, raiser2):
    try:
        func(  )
    except (General, Specific1, Specific2):     # Catch any of these.
        import sys
        print 'caught:', sys.exc_type

C:\python> python stringexc.py
caught: general
caught: specific1
caught: specific2

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.

Suppose you code a numeric programming library in Python, to be used by a large number of people. While you are writing your library, you identify two things that can go wrong with numbers in your code—division by zero, and numeric overflow. You document these as the two exceptions that your library may raise, and define them as simple strings in your code:

divzero = 'Division by zero error in library'
oflow   = 'Numeric overflow error in library'
...
raise divzero

Now, when people use your library, they will typically wrap calls to your functions or classes in try statements that catch your two exceptions (if they do not catch your exceptions, exceptions from the library kill their code):

import mathlib
...
try:
    mathlib.func(...)
except (mathlib.divzero, mathlib.oflow):
    ...report and recover...

This works fine and people use your library. Six months down the road, you revise your library; along the way, you identify a new thing that can go wrong—underflow—and add that as a new string exception:

divzero = 'Division by zero error in library'
oflow   = 'Numeric overflow error in library'
uflow   = 'Numeric underflow error in library'

Unfortunately, when you rerelease your code, you've just created a maintenance problem for your users. Assuming they list your exceptions explicitly, they have to now go back and change every place they call your library, to include the newly added exception name:

try:
    mathlib.func(...)
except (mathlib.divzero, mathlib.oflow, mathlib.uflow):
    ...report and recover...

Now, maybe this isn't the end of the world. If your library is used only in-house, you can make the changes yourself. You might also ship a Python script that tries to fix such code automatically (it would be a few dozen lines, and would guess right at least some of the time). If many people have to change their code each time you alter your exceptions set, though, this is not exactly the most polite of upgrade policies.

Your users might try to avoid this pitfall by coding empty except clauses:

try:
    mathlib.func(...)
except:                           # Catch everything here.
    ...report and recover...

The problem with this workaround is that it may catch more than they bargained for—even things like memory errors and system exits trigger exceptions, and you want such things to pass, not be caught and erroneously classified as a library error. As a rule of thumb, it's usually better to be specific than general in exception handlers (an idea we'll revisit in the gotchas).

So what to do, then? Class exceptions fix this dilemma completely. Rather than defining your library's exceptions as a simple set of strings, arrange them into a class tree, with a common superclass to encompass the entire category:

class NumErr: pass
class Divzero(NumErr): pass
class Oflow(NumErr): pass
...
raise DivZero(  )

This way, users of your library simply need to list the common superclass (i.e., category), to catch all of your library's exceptions—both now and in the future:

import mathlib
...
try:
    mathlib.func(...)
except mathlib.NumErr:
    ...report and recover...

When you go back and hack your code again, new exceptions are added as new subclasses of the common superclass:

class Uflow(NumErr): pass

The end result is that user code that catches your library's exceptions will keep working, unchanged. In fact, you are then free to add, delete, and change your exceptions arbitrarily in the future—as long as clients name the superclass, they are insulated from changes in your exceptions set. In other words, class exceptions provide a better answer to maintenance issues than strings do. Class-based exceptions can also support state retention and inheritance in ways that strings cannot—a concept we'll explore by example later in this section.

25.2.3 Built-in Exception Classes

We didn't really pull the prior section's examples out of thin air. Although user-defined exceptions may be identified by string or class objects, all built-in exceptions that Python itself may raise are predefined class objects, instead of strings. Moreover, they are organized into a shallow hierarchy with general superclass categories and specific subclass types, much like the exceptions class tree in the prior section.

All the familiar exceptions you've seen (e.g., SyntaxError) are really just predefined classes, available both as built-in names (in module __builtin__), and as attributes of the standard library module exceptions. In addition, Python organizes the built-in exceptions into a hierarchy, to support a variety of catching modes. For example:


Exception

Top-level root superclass of exceptions


StandardError

The superclass of all built-in error exceptions


ArithmeticError

The superclass of all numeric errors


OverflowError

A subclass that identifies a specific numeric error

And so on—you can read further about this structure in either the library manual, or the help text of the exceptions module (see Chapter 11 for help on help):

>>> import exceptions
>>> help(exceptions)
...lots of text omitted...

The built-in class tree allows you to choose how specific or general your handlers will be. For example, the built-in exception ArithmeticError is a superclass to more specific exceptions such as OverflowError and ZeroDivisionError. By listing ArithmeticError in a try, you will catch any kind of numeric error raised; by listing just OverflowError, you will intercept just that specific type of error, and no others.

Similarly, because StandardError is the superclass of all built-in error exceptions, you can generally use it to select between built-in errors and user-defined exceptions in a try:

try:
    action(  )
except StandardError:
    ...handle Python errors...
except:
    ...handle user exceptions...
else:
    ...handle no exception case...

You can also almost simulate an empty except clause (that catches everything) by catching root class Exception, but not quite—string exceptions, as well as standalone user-defined exceptions, are not subclasses of the Exception root class today.[1] Whether or not you will use categories in the built-in class tree, it serves as a good example; by using similar techniques for class exceptions in your own code, you can provide exception sets that are flexible, and easily modified.

[1] Note that current Python documentation says that "It is recommended that user-defined class-based exceptions be derived from the Exception class, although this is currently not enforced." That is, Exception subclasses are preferred to standalone exception classes, but not required. The defensive programmer might infer that it may be a good idea to adopt this policy anyhow.

Other than this, built-in exceptions are largely indistinguishable from strings. In fact, you normally don't need to care that they are classes, unless you assume built-in exception are strings and try to concatenate without converting (e.g., KeyError+"spam" fails, but str(KeyError)+"spam" works).

25.2.4 Specifying Exception Text

When we met string-based exceptions at the start of this section, we saw that the text of the string shows up in the standard error message when the exception is not caught. For an uncaught class exception, by default you get the class's name, and a not very pretty display of the instance object that was raised:

>>> class MyBad: pass

>>> raise MyBad(  )
Traceback (most recent call last):
  File "<pyshell#30>", line 1, in ?
    raise MyBad
MyBad: <__main__.MyBad instance at 0x00B58980>

To do better, define the __repr__ or __str__ string representation overload methods in your class, to return the string you want to display for your exception if it reaches the default handler at the top:

>>> class MyBad:
...     def __repr__(self):
...         return "Sorry--my mistake!"
... 
>>> raise MyBad(  )
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in ?
    raise MyBad(  )
MyBad: Sorry--my mistake!

The __repr__ overload method is called for printing, and string conversion requests made to your class's instances. See Section 21.4 in Chapter 21.

25.2.5 Sending Extra Data in Instances

Besides supporting flexible hierarchies, class exceptions also provide storage for extra state information as instance attributes. When a class-based exception is raised, Python automatically passes the class instance object along with the exception, as the extra data item. As for string exceptions, you can access the raised instance by listing an extra variable back in the try statement. This provides a natural hook for supplying data and behavior to the handler.

25.2.5.1 Example: extra data with classes and strings

Let's demonstrate the notion of extra data by an example, and compare string and class-based approaches along the way. A program that parses datafiles might signal a formatting error by raising an exception instance that is filled out with extra details about the error:

>>> class FormatError:
...     def __init__(self, line, file):
...         self.line = line
...         self.file = file
...
>>> def parser(  ):
...     # when error found
...     raise FormatError(42, file='spam.txt')
... 
>>> try:
...     parser(  )
... except FormatError, X:
...     print 'Error at', X.file, X.line
...
Error at spam.txt 42

In the except clause here, variable X is assigned a reference to the instance that was generated where the exception was raised. In practice, though, this isn't noticeably more convenient than passing compound objects (e.g., tuples, lists, or dictionaries) as extra data with string exceptions, and may not by itself be compelling enough to warrant class-based exceptions:

>>> formatError = 'formatError'

>>> def parser(  ):
...     # when error found
...     raise formatError, {'line':42, 'file':'spam.txt'}
...
>>> try:
...     parser(  )
... except formatError, X:
...     print 'Error at', X['file'], X['line']
...
Error at spam.txt 42

This time, variable X in the except clause is assigned the dictionary of extra details listed at the raise statement. The net effect is similar, without having to code a class along the way. The class approach might be more convenient, if the exception should also have behavior—the exception class can also define methods to be called in the handler:

class FormatError:
    def __init__(self, line, file):
        self.line = line
        self.file = file
    def logerror(self):
        log = open('formaterror.txt', 'a')
        print >> log, 'Error at', self.file, self.line

def parser(  ):
    raise FormatError(40, 'spam.txt')

try:
    parser(  )
except FormatError, exc:
    exc.logerror(  )

In such a class, methods (like logerror) may also be inherited from superclasses, and instance attributes (like line and file) provide a place to save state for use in later method calls. Here, we can mimic much of this effect by passing simple functions in the string-based approach:

formatError = "formatError"

def logerror(line, file):
    log = open('formaterror.txt', 'a')
    print >> log, 'Error at', file, line

def parser(  ):
    raise formatError, (41, 'spam.txt', logerror)

try:
    parser(  )
except formatError, data:
    data[2](data[0], data[1])        # Or simply: logerror(  )

Naturally, such functions would not participate in inheritance like class methods do, and would not be able to retain state in instance attributes (lambdas and global variables are usually the best we can do for stateful functions). We could, of course, pass a class instance in the extra data of string-based exceptions to achieve the same effect. But if we go this far to mimic class-based exceptions, we might as well adopt them—we'd be coding a class anyhow.

In general, the choice between string- and class-based exceptions is much like the choice to use classes at all; not every program requires the power of OOP. Sting-based exceptions are a simpler tool for simpler tasks. Class-based exceptions become most useful for defining categories, and in advanced applications that can benefit from state retention and attribute inheritance. As usual in Python, the choice to use OOP or not is mostly yours to make (although this might change in a future release of Python).

    [ Team LiB ] Previous Section Next Section