Exceptions

Exceptions

Source: this section is based on [ThinkCS]

Catching exceptions

Whenever a runtime error occurs, it creates an exception object. The program stops running at this point and Python prints out the traceback, which ends with a message describing the exception that occurred.

For example, dividing by zero creates an exception:

>>> print(55/0)
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

So does accessing a non-existent list item:

>>> a = []
>>> print(a[5])
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
IndexError: list index out of range

Or trying to make an item assignment on a tuple:

>>> tup = ("a", "b", "d", "d")
>>> tup[2] = "c"
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

In each case, the error message on the last line has two parts: the type of error before the colon, and specifics about the error after the colon.

Sometimes we want to execute an operation that might cause an exception, but we don't want the program to stop. In this case, we wish to handle the exception.

For example, we might prompt the user for the name of a file and then try to open it. If the file doesn't exist, we don't want the program to crash; we want to handle the exception. We can do this using the try statement to "wrap" a region of code:

filename = input("Enter a file name: ")
try:
    f = open(filename, "r")
    lines = f.readlines ()
    f.close ()
except:
    print("There is no file named", filename)

The try statement has three separate clauses, or parts, introduced by the keywords try ... except ... finally. Either the except or the finally clauses can be omitted, so the above code considers the most common version of the try statement first.

The try statement executes and monitors the statements in the first block. If no exceptions occur, it skips the block under the except clause. If any exception occurs, it executes the statements in the except clause and then continues.

We can use multiple except clauses to handle different kinds of exceptions (see the Errors and Exceptions lesson from Python creator Guido van Rossum's Python Tutorial for a more complete discussion of exceptions). So the program could do one thing if the file does not exist, but do something else if the file was in use by another program.

Raising our own exceptions

Can our program deliberately cause its own exceptions? If our program detects an error condition, we can also raise an exception ourselves. Here is an example that gets input from the user and checks that the number is non-negative:

def get_age():
    age = int(input("Please enter your age: "))
    if age < 0:
        # Create a new instance of an exception
        my_error = ValueError(str(age) + " is not a valid age")
        raise my_error
    return age

Line 5 creates an exception object, in this case, a ValueError object, which encapsulates specific information about the error. ValueError is a type of exception that is built into Python and is used by Python in case it encounters a value problem; we can also raise it ourselves.

Python's raise statement is somewhat similar to the return statement: it also returns information to a program that called this function. There are however some important differences between return statements and raise statements. This is illustrated by the following longer program.

def get_age():
    age = int(input("Please enter your age: "))
    if age < 0:
        # Create a new instance of an exception
        my_error = ValueError(str(age) + " is not a valid age")
        raise my_error
    return age

def contains_digit(s):
    """ pre: s is a string
        post: returns True if s contains a digit, and False otherwise
    """
    for l in s:
        if l in "0123456789":
            return True
    return False

def get_username():
    name = input("Please enter your username: ")
    if contains_digit(name):
      my_error = ValueError(name + " is not a valid name")
      raise my_error
    return name

def get_information ():
    age = get_age ()
    username = get_username ()
    return ( age, username )

try:
    age, username = get_information ()
    print ( "Your username is " + username + "; your age is  + str(age) )
except:
    print ( "Error entering information")

Note that in this program, function get_information () does not contain a try ... except block. In this program, the get_information function first calls get_age to ask for an age, and then get_username to ask for a name. What happens if the user does not enter a valid age? In this case, the get_age function raises an exception. However, the execution will not continue in the get_information function. As get_information does not handle exceptions, the program will backtrack towards the main part of the program, which contains a try ... except block. Here, it will not print the Your username is... message, but will rather print the Error entering information message.

Hence, where a return statement in a function will always return to the place where the function was called, a raise statement will break of multiple function calls, till it reaches a place where the exception is handled using a try ... except block. We call this "unwinding the call stack".

ValueError is one of the built-in exception types which most closely matches the kind of error we want to raise. The complete listing of built-in exceptions can be found at the Built-in Exceptions section of the Python Library Reference , again by Python's creator, Guido van Rossum. If Python stops with an error, it also displays the type of the error that caused it to stop.

If we would call the get_age function without try ... except block, we would get this output:

>>> get_age()
Please enter your age: 42
42
>>> get_age()
Please enter your age: -2
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "learn_exceptions.py", line 4, in get_age
    raise ValueError(str(age)+ " is not a valid age")
ValueError: -2 is not a valid age

The error message includes the exception type and the additional information that was provided when the exception object was first created.

We can also print the more specific error message using this code:

try:
    age, username = get_information ()
    print ( "Your username is " + username + " your age is " + str(age))
except ValueError as error:
    print ( error )

In this case, the try ... except block will only catch an exception of the type ValueError. It will store information regarding this exception, as created by raise statement in the error value, which we can subsequently print.

With statement

As pointed out earlier, a common situation in which exceptions are useful, is when working with files. We saw this code earlier:

filename = input("Enter a file name: ")
try:
    f = open(filename, "r")
    lines = f.readlines ()
    f.close ()
except:
    print("There is no file named", filename)

In this example, the program will print the message There is no file ... when the file does not exist.

Let us now consider this variation, in which we use the get_name function of the previous section:

filename = input("Enter a file name: ")
try:
    f = open(filename, "r")
    username = get_username ()
    for line in f:
        if line == username:
           print ( username + " found!" )
    f.close ()
except IOError:
    print("There is no file named", filename)
except ValueError:
    print("Incorrect name provided")

In this code, two different exceptions can occur: one is related to a file error, the other to the provision of an incorrect name. These two types of errors are distinguished by having two except statements; each of these will catch the corresponding type of error.

Many tricky things are happening in this code: we have one try ... except block for different types of errors, depending on whether or not the file exists we will ask for a name, and so on. One thing is happening in this code that makes it particularly undesirable. As stated earlier, it is considered good practice for a program to close every file that it opens.

In the program above, if an incorrect username is entered, the program will raise an exception, and jump towards printing the message Incorrect name provided without executing the close() instruction: after all, the close() statement is only executed after we have successfully finished the get_username() function.

To resolve this issue, the proper way to combine exception handling with file processing is as follows:

filename = input("Enter a file name: ")
try:
    with open(filename, "r") as f:
        username = get_username ()
        for line in f:
            if line == username:
                print ( "{0} found!".format ( username ) )
except IOError:
    print("There is no file named", filename)
except ValueError:
    print("Incorrect name provided")

In this code there is no close() statement any more! Instead, we have used the with open(filename, "r") as f: construction. What does this construction do? Essentially, it associates the result of open(filename, "r") to f, and executes the block of code

username = get_username ()
for line in f:
    if line == username:
        print ( "{0} found!".format ( username ) )

if the file was opened successfully. Two things can then happen:

  • the code executes successfully; in this case, the file will be closed automatically when the code is finished.
  • the code raises an execption; in this case, the file will be closed before the execution is passed on to the exception handler.

Hence, the with statement can be used to ensure that a file is automatically closed in all circumstances, whether good or bad.

Many Python programmers nowadays use with every time they open a file, as by using this statement, one does not need to think about closing a file any more: it will always happen after the specified piece of code is finished.

Let’s take a look at another example, which prints all the data in a file, line by line:

with open("testfile.txt") as file:
    data = file.readlines()
    for line in data:
        print(line, end='')

Notice that in the above example we didn’t use the file.close() method because the with statement will automatically call that for us upon execution. It really makes things a lot easier, doesn’t it?

Glossary

exception
An error that occurs at runtime.
handle an exception
To prevent an exception from causing our program to crash, by wrapping the block of code in a try ... except construct.
raise
To create a deliberate exception by using the raise statement.

References

[ThinkCS]How To Think Like a Computer Scientist --- Learning with Python 3

Page précédente Page suivante
<string>