Learning Python - Mark Lutz [466]
Catching categories
This code includes functions that raise instances of all three of our classes as exceptions, as well as a top-level try that calls the functions and catches General exceptions. The same try also catches the two specific exceptions, because they are subclasses of General.
Exception details
The exception handler here uses the sys.exc_info call—as we’ll see in more detail in the next chapter, it’s how we can grab hold of the most recently raised exception in a generic fashion. Briefly, the first item in its result is the class of the exception raised, and the second is the actual instance raised. In a general except clause like the one here that catches all classes in a category, sys.exc_info is one way to determine exactly what’s occurred. In this particular case, it’s equivalent to fetching the instance’s __class__ attribute. As we’ll see in the next chapter, the sys.exc_info scheme is also commonly used with empty except clauses that catch everything.
The last point merits further explanation. When an exception is caught, we can be sure that the instance raised is an instance of the class listed in the except, or one of its more specific subclasses. Because of this, the __class__ attribute of the instance also gives the exception type. The following variant, for example, works the same as the prior example:
class General(Exception): pass
class Specific1(General): pass
class Specific2(General): pass
def raiser0(): raise General()
def raiser1(): raise Specific1()
def raiser2(): raise Specific2()
for func in (raiser0, raiser1, raiser2):
try:
func()
except General as X: # X is the raised instance
print('caught:', X.__class__) # Same as sys.exc_info()[0]
Because __class__ can be used like this to determine the specific type of exception raised, sys.exc_info is more useful for empty except clauses that do not otherwise have a way to access the instance or its class. Furthermore, more realistic programs usually should not have to care about which specific exception was raised at all—by calling methods of the instance generically, we automatically dispatch to behavior tailored for the exception raised. More on this and sys.exc_info in the next chapter; also see Chapter 28 and Part VI at large if you’ve forgotten what __class__ means in an instance.
Why Exception Hierarchies?
Because 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 could achieve the same effects by coding a list of exception names in parentheses within the except clause:
try:
func()
except (General, Specific1, Specific2): # Catch any of these
...
This approach worked for the defunct string exception model too. For large or high exception hierarchies, however, it may be easier to catch categories using class-based categories than to list every member of a category in a single except clause. Perhaps more importantly, you can extend exception hierarchies by adding new subclasses without breaking existing code.
Suppose, for example, 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:
# mathlib.py
class Divzero(Exception): pass
class Oflow(Exception): pass
def func():
...
raise Divzero()
Now, when people use your library, they 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 will kill their code):
# client.py
import mathlib
try:
mathlib.func(...)