Exception Handling

Many primers push exception handling quite far back, often after covering object-oriented programming. We put them here in the control structures chapter because exception handling is relevant in both procedural and object-oriented programming, and because exception handling can cause the flow of execution to change dramatically, which certainly qualifies exception handlers as a kind of control structure.

An exception is an object that is "raised" (or "thrown") under some specific circumstances. When an exception is raised, the normal flow of execution ceases and the interpreter looks for a suitable exception handler to pass the exception to. It begins by looking at the enclosing block and works its way out. If no suitable exception handler is found in the current function, the interpreter will go up the call stack, looking for a handler in the function's caller, and if that fails in the caller's caller, and so on.

As the interpreter searches for a suitable exception handler, it may encounter finally blocks; any such blocks are executed, after which the search for an exception handler is resumed. (We use finally blocks for cleaning up—for example, to ensure that a file is closed, as we will see shortly.)

If a handler is found, the interpreter passes control to the handler, and execution continues from there. If, having gone all the way up the call stack to the top level, no handler is found, the application will terminate and report the exception that was the cause.

In Python, exceptions can be raised by built-in or library functions and methods, or by us in our code. The exceptions that are raised can be of any of the built-in exception types or our own custom exception types.

Exception handlers are blocks with the general syntax:

try:

suitel except exceptions:

suite2 else: suite3

Here, the code in suitel is executed, and if an exception occurs, control will pass to the except statement. If the except statement is suitable, suite2 will be executed; we will discuss what happens otherwise shortly. If no exception occurs, suite3 is executed after suitel is finished.

The except statement has more than one syntax; here are some examples:

except IndexError: pass except ValueError, e: pass except (IOError, OSError), e: pass except: pass

In the first case we are asking to handle IndexError exceptions but do not require any information about the exception if it is raised. In the second case we handle ValueError exceptions, and we want the exception object (which is put in variable e). In the third case we handle both IOError and OSError exceptions, and if either occurs, we also want the exception object, and again this is put in variable e. The last case should not be used, since it will catch any exception: Using such a broad exception handler is usually unwise because it will catch all kinds of exception, including those we don't expect, thereby masking logical errors in our code. Because we have used pass for the suites, if an exception is caught, no further action is taken, and execution will continue from the finally block if there is one, and then from the statement following the try block.

Python Exception Hierarchy
Figure 2.1 Some of Python's exception hierarchy

It is also possible for a single try block to have more than one exception handler:

try:

process() except IndexError, e:

print "Error: %s" % e except LookupError, e: print "Error: %s" % e

The order of the handlers is important. In this case, IndexError is a subclass of LookupError, so if we had LookupError first, control would never pass to the IndexError handler. This is because LookupError matches both itself and all its subclasses. Just like C++ and Java, when we have multiple exception handlers for the same try block they are examined in the order that they appear. This means that we must order them from most specific to least specific. Some of Python's exception hierarchy is shown in Figure 2.1; the least specific exception is at the top, going down to the most specific at the bottom.

Now that we have a broad overview of exceptions, let's see how their use compares with a more conventional error handling approach; this will also give us a feel for their use and syntax. We will look at two code snippets that have the same number of lines and that do exactly the same thing: They extract the first angle-bracketed item from a string. In both cases we assume that the variable text holds the string we are going to search.

# Testing for errors result = "" i = text.find("<") if i > -1:

j = text.find(">", i + 1) if j > -1:

# Exception handling try:

i = text.index("<") j = text.index(">", i + 1) result = text[i:j + 1] except ValueError:

result = "" print result

Both approaches ensure that result is an empty string if no angle-bracketed substring is found. However, the right-hand snippet focuses on the positive with each line in the try block able to assume that the previous lines executed correctly—because if they hadn't, they would have raised an exception and execution would have jumped to the except block.

If we were searching for a single substring, using find() would be more convenient than using the exception handling machinery; but as soon as we need to do two or more things that could fail, exception handling, as here, usually results in cleaner code with a clear demarcation between the code we are expecting to execute and the code we've written to cope with errors and out-cases.

When we write our own functions, we can have them raise exceptions in failure cases if we wish; for example, we could put a couple of lines at the beginning of the simplify() function we developed in a previous section:

def simplify(text, space=" \t\r\n\f", delete=""): if not space and not delete:

raise Exception, "Nothing to skip or delete"

This will work, but unfortunately, the Exception class (which is the conventional base class for Python exceptions) isn't specific to our circumstances. This is easily solved by creating our own custom exception and raising that instead:

class SimplifyError(Exception): pass def simplify(text, space=" \t\r\n\f", delete=""): if not space and not delete:

raise SimplifyError, "Nothing to skip or delete"

Exceptions are class instances, and although we don't cover classes until Chapter 3, the syntax for creating an exception class is so simple that there seems to be no reason not to show it here. The class statement has a similar structure to a def statement, with the class keyword, followed by the name, except that in the parentheses we put the base classes rather than parameter names. We've used pass to indicate an empty suite, and we have chosen to inherit Exception. We could have inherited from one of Exception's subclasses instead; for example, ValueError.

In practice, though, raising an exception in this particular case may be overkill. We could take the view that the function will always be called with space or delete or both nonempty, and we can assert this belief rather than use an exception:

def simplify(text, space=" \t\r\n\f", delete=""): assert space or delete

This will raise an AssertionError exception if both space and delete are empty, and probably expresses the logic of the function's preconditions better than the previous two attempts. If the exception is not caught (and an assertion should not be), the program will terminate and issue an error message saying that an AssertionError was the cause and providing a traceback that identifies the file and line where the assertion failed.

Another context where exception handling can be useful is breaking out of deeply nested loops. For example, imagine that we have a three-dimensional grid of values and we want to find the first occurrence of a particular target item. Here is the conventional approach:

found = False for x in range(len(grid)):

for z in range(len(grid[x][y])): if grid[x][y][z] == target: found = True break if found: break if found: break if found:

print "Found at (%d, %d, %d)" % (x, y, z) else:

print "Not found"

This is 15 lines long. It is easy to understand, but tedious to type and rather inefficient. Now we will use an approach that uses exception handling:

class FoundException(Exception): pass try:

for z in range(len(grid[x][y])): if grid[x][y][z] == target: raise FoundException except FoundException:

print "Found at (%d, %d, %d)" % (x, y, z) else:

print "Not found"

This version is only 11 lines long. If the target is found, we raise the exception and handle that situation. If no exception is raised, the try block's else suite is executed.

In some situations, we want some cleanup code to be called no matter what. For example, we may want to guarantee that we close a file or a network or database connection even if our code has a bug. This is achieved using a try ... finally block, as the next example shows:

filehandle = open(filename) try:

for line in filehandle: process(line)

finally:

filehandle.close()

Here we open a file with the given filename and get a file handle. We then iterate over the file handle—which is a generator and gives us one line at a time in the context of a for loop. If any exception occurs, the interpreter looks for the except or finally that is nearest in scope. In this case, it does not find an except, but it does find a finally, so the interpreter switches control to the finally suite and executes it. If no exception occurs, the finally block will be executed after the try suite has finished. So either way, the file will be closed.

Python versions prior to 2.5 do not support try ... except ... finally blocks. So if we need both except and finally we must use two blocks, a try . except and a try ... finally, with one nested inside the other. For example, in Python versions up to 2.4, the most robust way to open and process a file is like this:

try:

fh = open(fname) process(fh) except IOError, e:

print "I/O error: %s" % e finally: if fh:

fh.close()

This code makes use of things we have already discussed, but to make sure we have a firm grip on exception handling, we will consider the code in detail.

If the file can't be opened in the first place, the except block is executed and then the finally block—which will do nothing since the file handle will still be None because the file could not be opened. On the other hand, if the file is opened and processing commences, there might be an I/O error. If this happens, the except block is executed, and again control will then pass to the finally block, and the file will be closed.

If an exception occurs that is not an IOError, or an IOError subclass, for example, perhaps a ValueError occurs in our process() function—the interpreter will consider the except block to be unsuitable and will look for the nearest enclosing exception handler that is suitable. As it looks, the interpreter will first encounter the finally block which it will then execute, after which, (i.e., after closing the file), it will then look for a suitable exception handler.

If the file is opened and processing completes with no exception being raised, the except block is skipped, but the finally block is still executed since finally blocks are executed no matter what happens. So, in all cases, apart from the interpreter being killed by the user (or, in very rare cases, crashing), if the file was opened, it will be closed.

In Python 2.5 and later, we can use a simpler approach that has the same semantics because we can have try ... except ... finally blocks:

fh = open(fname) process(fh) except IOError, e:

print "I/O error: %s" % e finally: if fh:

fh.close()

Using this syntax, it is still possible to have an else block for when no exception occurred; it is placed after the last except block and before the one and only finally block. We will revisit this topic in the context of files in Chapter 6.

No matter what version of Python we use, finally blocks are always executed whether an exception occurs or not, exactly once, either when the try suite is finished, or when an exception is raised that shifts the flow of control outside the try block.

Python 2.6 (and Python 2.5 with a from_future_import with_statement statement) offers another approach entirely: "context managers". For file handling, we prefer the try ... finally approach, but in other cases, we prefer context managers. For example, we show how to use context managers for locking and unlocking read/write locks used by threads in Chapter 19.

Python

Python 2.6

Was this article helpful?

0 0
Tuberminator

Tuberminator

The main focus of this report is to show how to get involved in video marketing on the run, how to rank quickly on YouTube and Google using FREE semi-automatic tools and services. QUICKLY AND FREE. I will show methods and techniques I use to rank my videos, as well as free resources and tools to make video clips, to get backlinks and free traffic.

Get My Free Ebook


Responses

  • Daniele Udinesi
    How to catch exception in pyqt5?
    3 years ago

Post a comment