[ Team LiB ] |
25.2 Class-Based ExceptionsStrings 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:
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 ExampleLet'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 ClassesWe 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:
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.
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 TextWhen 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 InstancesBesides 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 stringsLet'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 ] |