Learning Python for Forensics
上QQ阅读APP看书,第一时间看更新

Try and except

The try and except syntax is used to catch and safely handle errors encountered during runtime. As a new developer, you'll become accustomed to people telling you that your scripts don't work. In Python, we use the try and except blocks to stop preventable errors from crashing our code. Please use the try and except blocks in moderation. Don't use them as if they were band-aids to plug up holes in a sinking ship—reconsider your original design instead and contemplate modifying the logic to better prevent errors.

We can try to run some line(s) of indented code and catch an exception or error with the except statement, such as TypeError or IndexError, and have our program executing other logic instead of crashing. Using these correctly will enhance the stability of our program. However, improper usage will not add any stability and can mask underlying issues in our code.

When an error is encountered, an error message is displayed to the user containing debug information and the program exits. This is an undesirable yet unavoidable scenario during the life cycle of our code. Rather than wrapping our entire code with error handling, we should place it in locations where we either anticipate errors occurring or find errors during testing.

For example, say we have some code that performs a mathematical calculation on two variables. If we anticipate that a user may accidentally enter non-integer or float values, we may want to wrap a try and except around the calculation to catch any TypeError exceptions that may arise. When we catch the error, we can try and convert the variables to integers with the class constructor method before trying again. If successful, we have saved our code from a preventable crash. Our catch scenario is not so vague that our program would still be able to execute if it had received a dictionary. In that case, we would want the script to crash and present debug information to the user.

It is often not advisable to cover many lines of code with one try and except block. Any line that has a reasonable chance of generating an error should be handled by its own try and except block with a solution for that specific line to ensure that we are properly handling the specific error. There are a few variations of the try and except block. In short, there are catch-all, catch-as-variable, and catch-specific types of block. The following pseudocode shows examples of how the blocks are formed:

# Basic try and except -- Catch-All
try:
# Line(s) of code
except:
# Line(s) of error-handling code
 
# Catch-As-Variable
try:
# Line(s) of code
except TypeError as e:
 print e.message
 # Line(s) of error-handling code
 
# Catch-Specific
try:
# Line(s) of code
except ValueError:
# Line(s) of error-handling code for ValueError exceptions

The catch-all or "bare" except will catch any error. This is often regarded as a poor coding practice as it can lead to program crashes or infinite loops. Catching an exception as a variable is useful in a variety of situations. The error message of the exception stored in e can be printed or written to a log by calling e.message—this can be particularly useful when an error occurs within a large multimodule program. In addition, the isinstance built-in function can be used to determine the type of error.

In the example later, we define two functions: giveError() and errorHandler(). The giveError() function tries to append 5 to the my_list variable. This variable has not yet been instantiated and will generate a NameError. In the except clause, we are catching a base Exception and storing it in the variable e. We then pass this exception object to our errorHandler() function, which we define later.

The errorHandler() function takes an exception object as its input. It checks whether the error is an instance of NameError or TypeError, or it passes otherwise. Based on the type of exception, it will print out the exception type and error message:

>>> def giveError():
... try:
... my_list.append(5)
... except Exception as e:
... errorHandler(e)
... 
>>> def errorHandler(error):
... if isinstance(error, NameError):
... print 'NameError:', error.message
... elif isinstance(error, TypeError):
... print 'TypeError:', error.message
... else:
... pass
... 
>>> giveError()
NameError: global name 'my_list' is not defined

Finally, the catch-specific try and except can be used to catch individual exceptions and has targeted error-handling code for that specific error. A scenario that might require a catch-specific try and except block is working with an object, such as list or dictionary, which may or may not be instantiated at that point in the program.

In the example given later, the results list does not exist when it is called in the function. Fortunately, we wrapped the append operation in a try and except to catch the NameError exceptions. When we do catch this exception, we first instantiate the results list as an empty list and then append the appropriate data before returning the list. Here is the example:

>>> def doubleData(data):
... for x in data:
... double_data = x*2
... try:
... # The results list does not exist the first time we try to append to it
... results.append(double_data)
... except NameError:
... results = []
... results.append(double_data)
... return results
... 
>>> my_results = doubleData(['a', 'b', 'c'])
>>> print my_results
['aa', 'bb', 'cc']

Raise

As our code can generate its own exceptions during execution, we can also manually trigger an exception to occur with the built-in raise() function. The raise() method is often used to raise an exception to the function that called it. While this may seem unnecessary, in larger programs, this can actually be quite useful.

Imagine a function, functionB(), which receives parsed data in the form of a packet from functionA(). Our functionB() does some further processing on said packet and then calls functionC() to continue to process the packet. If functionC() raises an exception back to functionB(), we might design some logic to alert the user of the malformed packet instead of trying to process it and producing faulty results. The following is pseudocode representing such a scenario:

001 import module
002
003 def main():
004     functionA(data)
005
006 def functionA(data_in):
007     try:
008         # parse data into packet
009         functionB(parsed_packet)
010     except Exception as e:
011         if isinstance(e, ErrorA):
012             # Fix this type of error
013             functionB(fixed_packet)
014             etc.
015 
016 functionB(packet):
017     # Process packet and store in processed_packet variable
018     try:
019         module.functionC(processed_packet)
020     except SomeError:
021         # Error testing logic
022         if type 1 error:
023             raise ErrorA()
024         elif type 2 error:
025             raise ErrorB()
026             etc.
027
028 if __name__ == '__main__':
029     main()

In addition, raising custom or built-in exceptions can be useful when dealing with exceptions that Python doesn't recognize on its own. Let's revisit the example of the malformed packet. When the second function received the raised error, we might design some logic that tests some possible sources of error. Depending on those results, we might raise different exceptions back to the calling function, functionA().

When raising a built-in exception, make sure to use an exception that most closely matches the error. For example, if the error revolves around an index issue, use the IndexError exception. When raising an exception, we should pass in a string containing a description of the error. This string should be descriptive and help the developer identify the issue, unlike the following string used. The adage "do what we say not what we do" applies here:

>>> def raiseError():
... raise TypeError('This is a TypeError')
... 
>>> raiseError()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 2, in raiseError
TypeError: This is a TypeError