See All Titles |
![]() ![]() Detecting and Handling ExceptionsExceptions can be detected by incorporating them as part of a try statement. Any code suite of a try statement will be monitored for exceptions. There are two main forms of the try statement: try-except and try-finally. These statements are mutually exclusive, meaning that you pick only one of them. A try statement is either accompanied by one or more except clauses or exactly one finally clause. (There is no such thing as a hybrid "try-except-finally.") try-except statements allow one to detect and handle exceptions. There is even an optional else clause for situations where code needs to run only when no exceptions are detected. Meanwhile, try-finally statements allow only for detection and processing of any obligatory clean-up (whether or not exceptions occur), but otherwise has no facility in dealing with exceptions. try-except StatementThe try-except statement (and more complicated versions of this statement) allows you to define a section of code to monitor for exceptions and also provides the mechanism to execute handlers for exceptions. The syntax for the most general try-except statement looks like this: try: try_suite # watch for exceptions here except Exception: except_suite # exception-handling code Let us give one example, then explain how things work. We will use our IOError example from above. We can make our code more robust by adding a try-except "wrapper" around the code: >>> try: … f = open('blah') … except IOError: … print 'could not open file' … could not open file As you can see, our code now runs seemingly without errors. In actuality, the same IOError still occurred when we attempted to open the nonexistent file. The difference? We added code to both detect and handle the error. When the IOError exception was raised, all we told the interpreter to do was to output a diagnostic message. The program continues and does not "bomb out" as our earlier example—a minor illustration of the power of exception handling. So what is really happening codewise? During run-time, the interpreter attempts to execute all the code within the try statement. If an exception does not occur when the code block has completed, execution resumes past the except statement. When the specified exception named on the except statement does occur, control flow immediately continues in the handler (all remaining code in the try clause is skipped). In our example above, we are catching only IOError exceptions. Any other exception will not be caught with the handler we specified. If, for example, you want to catch an OSError, you have to add a handler for that particular exception. We will elaborate on the try-except syntax more as we progress further in this chapter. NOTE The remaining code in the try suite from the point of the exception is never reached (hence never executed). Once an exception is raised, the race is on to decide on the continuing flow of control. The remaining code is skipped, and the search for a handler begins. If one is found, the program continues in the handler. If the search is exhausted without finding an appropriate handler, the exception is then propagated to the caller's level for handling, meaning the stack frame immediately preceding the current one. If there is no handler at the next higher level, the exception is yet again propagated to its caller. If the top level is reached without an appropriate handler, the exception is considered unhandled, and the Python interpreter will display the traceback and exit. Wrapping a Built-in FunctionWe will now present an interactive example—starting with the bare necessity of detecting an error, then building continuously on what we have to further improve the robustness of our code. The premise is in detecting errors while trying to convert a numeric string to a proper (numeric object) representation of its value. The float() built-in function has a primary purpose of converting any numeric type to a float. In Python 1.5, float() was given the added feature of being able to convert a number given in string representation to an actual float value, obsoleting the use of the atof() function of the string module. Readers with older versions of Python may still use string.atof(), replacing float(), in the examples we use here. >>> float(12345) 12345.0 >>> float('12345') 12345.0 >>> float('123.45e67') 1.2345e+069 Unfortunately, float() is not very forgiving when it comes to bad input: >>> float('abcde') Traceback (innermost last): File "<stdin>", line 1, in ? float('abcde') ValueError: invalid literal for float(): abcde >>> >>> float(['this is', 1, 'list']) Traceback (innermost last): File "<stdin>", line 1, in ? float(['this is', 1, 'list']) TypeError: object can't be converted to float Notice in the errors above that float() does not take too kindly to strings which do not represent numbers or non-strings. Specifically, if the correct argument type was given (string type) but that type contained an invalid value, the exception raised would be ValueError because it was the value that was improper, not the type. In contrast, a list is a bad argument altogether, not even being of the correct type; hence, TypeError was thrown. Our exercise is to call float() "safely," or in a more "safe manner," meaning that we want to ignore error situations because they do not apply to our task of converting numeric string values to floating point numbers, yet are not severe enough errors that we feel the interpreter should abandon execution. To accomplish this, we will create a "wrapper" function, and, with the help of try-except, create the environment that we envisioned. We shall call it safe_float(). In our first iteration, we will scan and ignore only ValueErrors, because they are the more likely culprit. TypeErrors rarely happen since somehow a non-string must be given to float(). def safe_float(object): try: return float(object) except ValueError: pass The first step we take is to just "stop the bleeding." In this case, we make the error go away by just "swallowing it." In other words, the error will be detected, but since we have nothing in the except suite (except the pass statement, which does nothing but serve as a syntactical placeholder for where code is supposed to go), no handling takes place. We just ignore the error. One obvious problem with this solution is that we did not explicitly return anything to the function caller in the error situation. Even though None is returned (when a function does not return any value explicitly, i.e., completing execution without encountering a return object statement), we give little or no hint that anything wrong took place. The very least we should do is to explicitly return None so that our function returns a value in both cases and makes our code somewhat easier to understand: def safe_float(object): try: retval = float(object) except ValueError: retval = None return retval Bear in mind that with our change above, nothing about our code changed except that we used one more local variable. In designing a well-written application programmer interface (API), you may have kept the return value more flexible. Perhaps you documented that if a proper argument was passed to safe_float(), then indeed, a floating point number would be returned, but in the case of an error, you chose to return a string indicating the problem with the input value. We modify our code one more time to reflect this change: def safe_float(object): try: retval = float(object) except ValueError: retval = 'could not convert non-number to float' return retval The only thing we changed in the example was to return an error string as opposed to just None. We should take our function out for a "test drive" to see how well it works so far: >>> safe_float('12.34') 12.34 >>> safe_float('bad input') 'could not convert non-number to float' We made a good start—now we can detect invalid string input, but we are still vulnerable to invalid objects being passed in: >>> safe_float({'a': 'Dict'}) Traceback (innermost last): File "<stdin>", line 1, in ? File "safeflt.py", line 28, in safe_float retval = float(object) TypeError: object can't be converted to float We will address this final shortcoming momentarily, but before we further modify our example, we would like to highlight the flexibility of the try-except syntax, especially the except statement, which comes in a few more flavors. try Statement with Multiple exceptsEarlier in this chapter, we introduced the following general syntax for except: except Exception: suite_for_exception_Exception The except statement in such formats specifically detects exceptions named Exception. You can chain multiple except statements together to handle different types of exceptions with the same try: except Exception1: suite_for_exception_Exception1 except Exception2: suite_for_exception_Exception2 : This same try clause is attempted, and if there is no error, execution continues, passing all the except clauses. However, if an exception does occur, the interpreter will look through your list of handlers attempting to match the exception with one of your handlers (except clauses). If one is found, execution proceeds to that except suite. Our safe_float() function has some brains now to detect specific exceptions. Even smarter code would handle each appropriately. To do that, we have to have separate except statements, one for each exception type. That is no problem as Python allows except statements can be chained together. Any reader familiar with popular third-generation languages (3GLs) will no doubt notice the similarities to the switch/case statement which is absent in Python. We will now create separate messages for each error type, providing even more detail to the user as to the cause of his or her problem: def safe_float(object): try: retval = float(object) except ValueError: retval = 'could not convert non-number to float' except TypeError: retval = 'object type cannot be converted to float' return retval Running the code above with erroneous input, we get the following: >>> safe_float('xyz') 'could not convert non-number to float' >>> safe_float(()) 'argument must be a string' >>> safe_float(200L) 200.0 >>> safe_float(45.67000) 45.67 except Statement with Multiple ExceptionsWe can also use the same except clause to handle multiple exceptions. except statements which process more than one exception require that the set of exceptions be contained in a tuple: except (Exception1, Exception2): suite_for_Exception1_and_Exception2 The above syntax example illustrates how two exceptions can be handled by the same code. In general, any number of exceptions can follow an except statement as long as they are all properly enclosed in a tuple: except (Exception1[, Exception2[, … ExceptionN…]]): suite_for_exceptions_Exception1_to_ExceptionN If for some reason, perhaps due to memory constraints or dictated as part of the design that all exceptions for our safe_float() function must be handled by the same code, we can now accommodate that requirement: def safe_float(object): try: retval = float(object) except (ValueError, TypeError): retval = 'argument must be a number or numeric string' return retval Now there is only the single error string returned on erroneous input: >>> safe_float('Spanish Inquisition') 'argument must be a number or numeric string' >>> safe_float([]) 'argument must be a number or numeric string' >>> safe_float('1.6') 1.6 >>> safe_float(1.6) 1.6 >>> safe_float(932) 932.0 try-except with No Exceptions NamedThe final syntax for try-except we are going to present is one which does not specify an exception on the except header line: try: try_suite # watch for exceptions here except: except_suite # handles all exceptions Although this code "catches the most exceptions," it does not promote good Python coding style. One of the chief reasons is that it does not take into account the potential root causes of problems which may generate exceptions. Rather than investigating and discovering what types of errors may occur and how they may be prevented from happening, this type of code "turns the blind eye," thereby ignoring the possible causes (and remedies). Also see the Core Style featured in this section. NOTE The try-except statement has been included in Python to provide a powerful mechanism for programmers to track down potential errors and to perhaps provide logic within the code to handle situations where it may not otherwise be possible, for example in C. The main idea is to minimize the number of errors and still maintain program correctness. As with all tools, they must be used properly. One incorrect use of try-except is to serve as a giant bandage over large pieces of code. By that we mean putting large blocks, if not your entire source code, within a try and/or have a large generic except to "filter" any fatal errors by ignoring them: # this is really bad code try: large_block_of_code # bandage of large piece of code except: pass # blind eye ignoring all errors Obviously, errors cannot be avoided, and the job of try-except is to provide a mechanism whereby an acceptable problem can be remedied or properly dealt with, and not be used as a filter. The construct above will hide many errors, but this type of usage promotes a poor engineering practice that we certainly cannot endorse. Bottom line: Avoid using try-except around a large block of code with a pass just to hide errors. Instead, handle specific exceptions and enclose only deserving code in your try clause, as evidenced by some of the constructs we used for the safe_float() example in this section. "Exceptional Arguments"No, the title of this section has nothing to do with having a major fight. Instead, we are referring to the fact that exception may have arguments are passed along to the exception handler when they are raised. When an exception is raised, parameters are generally provided as an additional aid for the exception handler. Although arguments to exceptions are optional, the standard built-in exceptions do provide at least one argument, an error string indicating the cause of the exception. Exception parameters can be ignored in the handler, but the Python provides syntax for saving this value. To access any provided exception argument, you must reserve a variable to hold the argument. This argument is given on the except header line and follows the exception type you are handling. The different syntaxes for the except statement can be extended to the following: # single exception except Exception, Argument: suite_for_Exception_with_Argument # multiple exceptions except (Exception1, Exception2, …, ExceptionN), Argument: suite_for_Exception1_to_ExceptionN_with_Argument Unless a string exception (see Section 10.4) was raised, Argument is a class instance containing diagnostic information from the code raising the exception. The exception arguments themselves go into a tuple which is stored as an attribute of the class instance, an instance of the exception class from which it was instantiated. In the first alternate syntax above, Argument would be an instance of the Exception class. For most standard built-in exceptions, that is, exceptions derived from StandardError, the tuple consists of a single string indicating the cause of the error. The actual exception name serves as a satisfactory clue, but the error string enhances the meaning even more. Operating system or other environment type errors, i.e., IOError, will also include an operating system error number which precedes the error string in the tuple. Whether an Argument is merely a string or a combination of an error number and a string, calling str(Argument) should present a human-readable cause of an error. The only caveat is that not all exceptions raised in third-party or otherwise external modules adhere to this standard protocol (or error string or (error number, error string). We recommend to follow such a standard when raising your own exceptions (see Core Style note). NOTE When you raise built-in exceptions in your own code, try to follow the protocol established by the existing Python code as far as the error information that is part of the tuple passed as the exception argument. In other words, if you raise a ValueError, provide the same argument information as when the interpreter raises a ValueError exception, and so on. This helps keep the code consistent and will prevent other code which uses your module from breaking. The example below is when an invalid object is passed to the float() built-in function, resulting in a TypeError exception: >>> try: … float(['float() does not', 'like lists', 2]) … except TypeError, diag:# capture diagnostic info … pass … >>> type(diag) <type 'instance'> >>> >>> print diag object can't be converted to float The first thing we did was cause an exception to be raised from within the try statement. Then we passed cleanly through by ignoring but saving the error information. Calling the type() built-in function, we were able to confirm that our exception was indeed an instance. Finally, we displayed the error by calling print with our diagnostic exception argument. To obtain more information regarding the exception, we can use the special __class__ instance attribute which identifies which class an instance was instantiated from. Class objects also have attributes, such as a documentation string and a string name which further illuminate the error type: >>> diag # exception instance object <exceptions.TypeError instance at 8121378> >>> diag.__class__ # exception class object <class exceptions.TypeError at 80f6d50> >>> diag.__class__.__doc__ # exception class documentation string 'Inappropriate argument type.' >>> diag.__class__.__name__ # exception class name 'TypeError' As we will discover in Chapter 13—Classes and OOP—the special instance attribute __class__ exists for all class instances, and the __doc__ class attribute is available for all classes which define their documentation strings. We will now update our safe_float() one more time to include the exception argument which is passed from the interpreter from within float() when exceptions are generated. In our last modification to safe_float(), we merged both the handlers for the ValueError and TypeError exceptions into one because we had to satisfy some requirement. The problem, if any, with this solution is that no clue is given as to which exception was raised nor what caused the error. The only thing returned is an error string which indicated some form of invalid argument. Now that we have the exception argument, this no longer has to be the case. Because each exception will generate its own exception argument, if we chose to return this string rather than a generic one we made up, it would provide a better clue as to the source of the problem. In the following code snippet, we replace our single error string with the string representation of the exception argument. def safe_float(object): try: retval = float(object) except (ValueError, TypeError), diag: retval = str(diag) return retval Upon running our new code, we obtain the following (different) messages when providing improper input to safe_float(), even if both exceptions are managed by the same handler: >>> safe_float('xyz') 'invalid literal for float(): xyz' >>> safe_float({}) 'object can't be converted to float' Using Our Wrapped Function in an ApplicationWe will now feature safe_float() in a mini application which takes a credit card transaction data file (carddata.txt) and reads in all transactions, including explanatory strings. Here are the contents of our example carddata.txt file: % cat carddata.txt # carddata.txt previous balance 25 debits 21.64 541.24 25 credits -25 -541.24 finance charge/late fees 7.30 5 Our program, cardrun.py, is given in Example 10.1. Example 10.1. Credit Card Transactions (cardrun.py)We use safe_float() to process a set of credit card transactions given in a file and read in as strings. A log file tracks the processing. <$nopage> 001 1 #!/usr/bin/env python 002 2 003 3 import types 004 4 005 5 def safe_float(object): 006 6 'safe version of float()' 007 7 try: <$nopage> 008 8 retval = float(object) 009 9 except (ValueError, TypeError), diag: 010 10 retval = str(diag) 011 11 return retval 012 12 013 13 def main(): 014 14 'handles all the data processing' 015 15 log = open('cardlog.txt', 'w') 016 16 try: <$nopage> 017 17 ccfile = open('carddata.txt', 'r') 018 18 except IOError: 019 19 log.write('no txns this month\n') 020 20 log.close() 021 21 return <$nopage> 022 22 023 23 txns = ccfile.readlines() 024 24 ccfile.close() 025 25 total = 0.00 026 26 log.write('account log:\n') 027 27 028 28 for eachTxn in txns: 029 29 result = safe_float(eachTxn) 030 30 if type(result) == types.FloatType: 031 31 total = total + result 032 32 log.write('data… processed\n') 033 33 else: <$nopage> 034 34 log.write('ignored: %s' % result) 035 35 print '$%.2f (new balance)' % (total) 036 36 log.close() 037 37 038 38 if __name__ == '__main__': 039 39 main() 040 <$nopage> Lines 1 – 3The script starts by importing the types modules, which contains Type objects for the Python types. That is why we direct them to standard error instead. Lines 5 – 11This chunk of code contains the body of our safe_float() function. Lines 13 – 36The core part of our application performs three major tasks: (1) read the credit card data file, (2) process the input, and (3) display the result. Lines 16–24 perform the extraction of data from the file. You will notice that there is a try-except statement surrounding the file open. A log file of the processing is also kept. In our example, we are assuming the log file can be opened for write without any problems. You will find that our progress is kept by the log. If the credit card data file cannot be accessed, we will assume there are no transactions for the month (lines 18–21). The data is then read into the txns (transactions) list where it is iterated over in lines 28–34. After every call to safe_float(), we check the result type using the types module. The types module contains items of each type, named appropriately typeType, so that direct comparisons can be performed with results that determine an object's type. In our example, we check to see if safe_float() returns a string or float. Any string indicates an error situation with a string that could not be converted to a number, while all other values are floats which can be added to the running subtotal. The final new balance is then displayed as the final line of the main() function. Lines 38 – 39These lines represent the general "start only if not imported" functionality. Upon running our program, we get the following output: % cardrun.py $58.94 (new balance) Taking a peek at the resulting log file (cardlog.txt), we see that it contains the following log entries after cardrun.py processed the transactions found in carddata.txt: % cat cardlog.txt account log: ignored: invalid literal for float(): # carddata.txt ignored: invalid literal for float(): previous balance data… processed ignored: invalid literal for float(): debits data… processed data… processed data… processed ignored: invalid literal for float(): credits data… processed data… processed ignored: invalid literal for float(): finance charge/ late fees data… processed data… processed else ClauseWe have seen the else statement with other Python constructs such as conditionals and loops. With respect to try-except statements, its functionality is not that much different from anything else you have seen: The else clause executes if no exceptions were detected in the preceding try suite. All code within the try suite must have completed successfully (i.e., concluded with no exceptions raised) before any code in the else suite begins execution. Here is a short example in Python pseudocode: import 3rd_party_module log = open('logfile.txt', 'w') try: 3rd_party_module.function() except: log.write("*** caught exception in module\n") else: log.write("*** no exceptions caught\n") log.close() In the above example, we import an external module and test it for errors. A log file is used to determine whether there were defects in the third-party module code. Depending on whether an exception occurred during execution of the external function, we write differing messages to the log. try-except Kitchen SinkWe can combine all the varying syntaxes that we have seen so far in this chapter to highlight all the different ways you can use try-except-else: try: try_suite except Exception1: suite_for_Exception1 except (Exception2, Exception3, Exception4): suite_for_Exceptions_2_3_and_4 except Exception5, Argument5: suite_for_Exception5_plus_argument except (Exception6, Exception7), Argument67: suite_for_Exceptions6_and_7_plus_argument except: suite_for_all_other_exceptions else: no_exceptions_detected_suite try-finally StatementThe try-finally statement differs from its try-except brethren in that it is not used to handle exceptions. Instead it is used to maintain consistent behavior regardless of whether or not exceptions occur. The finally suite executes regardless of an exception being triggered within the try suite. try: try_suite finally: finally_suite # executes regardless of exceptions When an exception does occur within the try suite, execution jumps immediately to the finally suite. When all the code in the finally suite completes, the exception is re-raised for handling at the next higher layer. Thus it is common to see a try-finally nested as part of a try-except suite. One place where we can add a try-finally statement is by improving our code in cardrun.py so that we catch any problems which may arise from reading the data from the carddata.txt file. In the current code in Example 10.1, we do not detect errors during the read phase (using readlines()): try: ccfile = open('carddata.txt') except IOError: log.write('no txns this month\n') log.close() return txns = ccfile.readlines() ccfile.close() It is possible for readlines() to fail for any number of reasons, one of which is if carddata.txt was a file on the network (or a floppy) that became inaccessible. Regardless, we should improve this piece of code so that the entire input of data is enclosed in the try clause: try: ccfile = open('carddata.txt') txns = ccfile.readlines() ccfile.close() except IOError: log.write('no txns this month\n') log.close() return All we did was to move the readlines() and close() method calls to the try suite. Although our code is more robust now, there is still room for improvement. Notice what happens if there was an error of some sort. If the open succeeds but for some reason the readlines() call does not, the exception will continue with the except clause. No attempt is made to close the file. Wouldn't it be nice if we closed the file regardless of whether an error occurred or not? We can make it a reality using try-finally: try: ccfile = open('carddata.txt') try: txns = ccfile.readlines() finally: ccfile.close() except IOError: log.write('no txns this month\n') log.close() return Now our code is more robust than ever. Let us take a look at another familiar example, calling float() with an invalid value. We will use print statements to show you the flow of execution within the try-except and try-finally clauses. We present tryfin.py in Example 10.2. Example l0.2. Testing the try-finally Statement (tryfin.py)This small script simply illustrates the flow of control when using a try-finally statement embedded within the try clause of a try-except statement. <$nopage> 001 1 #!/usr/bin/env python 002 2 003 3 try: <$nopage> 004 4 print 'entering 1st try' 005 5 try: <$nopage> 006 6 print 'entering 2nd try' 007 7 float('abc') 008 8 009 9 finally: <$nopage> 010 10 print 'doing finally' 011 11 012 12 except ValueError: 013 13 print 'handling ValueError' 014 14 015 15 print 'finishing execution' 016 <$nopage> Running this code, we get the following output: % tryfin.py entering 1st try entering 2nd try doing finallyhandling ValueError finishing execution One final note: If the code in the finally suite raises another exception, or is aborted due to a return,break, or continue statement, the original exception is lost and cannot be re-raised. Quick review: The try-finally statement presents a way to detect errors but ignore other than cleanup, and passes the exception up to higher layers for possible handling. NOTE Currently, continue statements inside a try suite are not allowed due to the current implementation of the Python bytecode generator (see FAQ 6.28). This restriction has been lifted in JPython, however. The proper workaround is to use an if-else in place of a continue. A more interesting solution involves creating a special exception handler to issue the continue (since continue statements are fine inside an except clause), as illustrated by the following Python pseudocode: # create our own exception (see section 10.9) class Continue(Exception): pass # begin our loop some_loop: pseudocode for a loop # try clause inside some_loop try: if skip_rest_of_loop_expr: raise Continue …code we do not want executed if skip_rest_of_loop_expr is true… except Continue: # continue proxy (as except clause) continue # start next some_loop iteration except SomeError: # handle real exceptions : We will look at the raise statement later on in this chapter, but, as you can probably tell, raise is the statement that lets programmers explicitly raise exceptions in Python.
|
© 2002, O'Reilly & Associates, Inc. |