--- /dev/null
+{
+ "metadata": {
+ "name": ""
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+ {
+ "cells": [
+ {
+ "cell_type": "heading",
+ "level": 1,
+ "metadata": {},
+ "source": [
+ "Basic Programming Using Python: Handling Errors"
+ ]
+ },
+ {
+ "cell_type": "heading",
+ "level": 2,
+ "metadata": {},
+ "source": [
+ "Objectives"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* Explain what an assertion is, and write assertions with customized error messages.\n",
+ "* Distinguish between pre-conditions, post-conditions, and invariants.\n",
+ "* Explain what defensive programming is, and write functions and programs in this way.\n",
+ "* Correctly raise and handle exceptions."
+ ]
+ },
+ {
+ "cell_type": "heading",
+ "level": 2,
+ "metadata": {},
+ "source": [
+ "Defensive Programming"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We made several mistakes while writing the programs in our first few lessons.\n",
+ "How can we be sure that there aren't still errors lurking in the code we have?\n",
+ "And how can we guard against introducing new errors in code as we modify it?\n",
+ "\n",
+ "The first step is to use [defensive programming](glossary.html#defensive_programming),\n",
+ "i.e.,\n",
+ "to assume that mistakes *will* happen\n",
+ "and to guard against them.\n",
+ "One way to do this is to add [assertions](glossary.html#assertion) to our code\n",
+ "so that it checks itself as it runs.\n",
+ "We met assertions in [an earlier lesson](python-3-conditionals-defensive.ipynb);\n",
+ "as we said then,\n",
+ "each one states something that must be true at a certain point in the program's execution,\n",
+ "and optionally includes a customized error message explaining what's gone wrong if it fails:\n",
+ "\n",
+ "```\n",
+ "assert width > 0, 'Grid width must be positive'\n",
+ "assert 0 <= ablation(x, y) <= 1.0, 'Ablation must be normalized'\n",
+ "```\n",
+ "\n",
+ "Programs like the Firefox browser are littered with assertions:\n",
+ "10-20% of the code they contain\n",
+ "are there to check that the other 80-90% are working correctly.\n",
+ "Broadly speaking,\n",
+ "assertions fall into three categories:\n",
+ "\n",
+ "- A [precondition](glossary.html#precondition) is something that must be true\n",
+ " in order for a piece of code to work correctly.\n",
+ "- A [postcondition](glossary.html#postcondition) is something that must be true\n",
+ " at the end of a piece of code if it worked correctly.\n",
+ "- An [invariant](glossary.html#invariant) is something that is always true\n",
+ " at a particular point inside a piece of code."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For example,\n",
+ "suppose we are representing rectangles using a list of four coordinates `[x0, y0, x1, y1]`.\n",
+ "In order to do some calculations,\n",
+ "we need to normalize the rectangle so that it is at the origin\n",
+ "and 1.0 units long on its longest axis.\n",
+ "This function does that:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "def normalize_rectangle(rect):\n",
+ " x0, y0, x1, y1 = rect\n",
+ " assert x0 < x1, 'Invalid X coordinates'\n",
+ " assert y0 < y1, 'Invalid Y coordinates'\n",
+ "\n",
+ " dx = x1 - x0\n",
+ " dy = y1 - y0\n",
+ " if dx > dy:\n",
+ " scaled = float(dy) / dx\n",
+ " upper_x, upper_y = 1.0, scaled\n",
+ " else:\n",
+ " scaled = float(dx) / dy\n",
+ " upper_x, upper_y = scaled, 1.0\n",
+ "\n",
+ " assert 0 < upper_x <= 1.0, 'Calculated upper X coordinate invalid'\n",
+ " assert 0 < upper_y <= 1.0, 'Calculated upper Y coordinate invalid'\n",
+ "\n",
+ " return [0, 0, upper_x, upper_y]"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [],
+ "prompt_number": 7
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first two assertions check that we've been given a legal rectangle,\n",
+ "while the last two check the output we're about to return to our caller.\n",
+ "Strictly speaking these post-conditions are redundant:\n",
+ "if the inputs and calculations are correct,\n",
+ "the last two assertions should always hold.\n",
+ "But those are pretty big ifs,\n",
+ "and having the program check itself can save us a lot of hunting around later."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "<hr/>\n",
+ "### *Assertions and Bugs*\n",
+ "\n",
+ "<em>\n",
+ "A rule that many programmers follow is, \"Bugs become assertions.\"\n",
+ "Whenever we fix a bug in a program,\n",
+ "we should add some assertions to the program at that point to catch the bug if it reappears.\n",
+ "After all,\n",
+ "if we made the mistake once,\n",
+ "then we (or someone else) might well make it again.\n",
+ "Few things are as frustrating as\n",
+ "having someone delete several carefully-crafted lines of code that fixed a subtle problem\n",
+ "because they didn't realize what problem those lines were there to fix.\n",
+ "</em>\n",
+ "<hr/>"
+ ]
+ },
+ {
+ "cell_type": "heading",
+ "level": 2,
+ "metadata": {},
+ "source": [
+ "Handling Errors"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Assertions help us catch errors in our code,\n",
+ "but things can go wrong for other reasons.\n",
+ "In particular,\n",
+ "errors may have external causes,\n",
+ "like missing or badly-formatted files.\n",
+ "Most modern programming languages allow programmers to use [exceptions](glossary.html#exception) to handle errors\n",
+ "in a way that separates what's supposed to happen if everything goes right\n",
+ "from what the program should do if something goes wrong.\n",
+ "Doing this makes both cases easier to read and understand.\n",
+ "\n",
+ "For example,\n",
+ "here's a small piece of code that tries to read parameters and a grid from two separate files,\n",
+ "and reports an error if either goes wrong:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "try:\n",
+ " params = read_params(param_file)\n",
+ " grid = read_grid(grid_file)\n",
+ "except:\n",
+ " log.error('Failed to read input file(s)')\n",
+ " sys.exit(ERROR)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We join the normal case and the error-handling code using the keywords `try` and `except`.\n",
+ "These work together like `if` and `else`:\n",
+ "the statements under the `try` are what should happen if everything works,\n",
+ "while the statements under `except` are what the program should do if something goes wrong."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We have actually seen exceptions before without knowing it,\n",
+ "since by default,\n",
+ "when an exception occurs,\n",
+ "Python prints it out and halts our program.\n",
+ "For example,\n",
+ "trying to open a nonexistent file triggers a type of exception called an `IOError`,\n",
+ "while an out-of-bounds index to a list triggers an `IndexError`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "open('nonexistent-file.txt', 'r')"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "IOError",
+ "evalue": "[Errno 2] No such file or directory: 'nonexistent-file.txt'",
+ "output_type": "pyerr",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mIOError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m<ipython-input-8-58cbde3dd63c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'nonexistent-file.txt'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'r'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;31mIOError\u001b[0m: [Errno 2] No such file or directory: 'nonexistent-file.txt'"
+ ]
+ }
+ ],
+ "prompt_number": 8
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "values = [0, 1, 2]\n",
+ "print values[999]"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "IndexError",
+ "evalue": "list index out of range",
+ "output_type": "pyerr",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m<ipython-input-9-7fed13afc650>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mprint\u001b[0m \u001b[0mvalues\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m999\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;31mIndexError\u001b[0m: list index out of range"
+ ]
+ }
+ ],
+ "prompt_number": 9
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can use `try` and `except` to deal with these errors ourselves\n",
+ "if we don't want the program simply to fall over.\n",
+ "Here,\n",
+ "for example,\n",
+ "we put our attempt to open a nonexistent file inside a `try`,\n",
+ "and in the `except`, we print a not-very-helpful error message:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [
+ "try:\n",
+ " reader = open('nonexistent-file.txt', 'r')\n",
+ "except IOError:\n",
+ " print 'Whoops!'"
+ ],
+ "language": "python",
+ "metadata": {},
+ "outputs": [
+ {
+ "output_type": "stream",
+ "stream": "stdout",
+ "text": [
+ "Whoops!\n"
+ ]
+ }
+ ],
+ "prompt_number": 10
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When Python executes this code,\n",
+ "it runs the statement inside the `try`.\n",
+ "If that works, it skips over the `except` block without running it.\n",
+ "If an exception occurs inside the `try` block,\n",
+ "though,\n",
+ "Python compares the type of the exception to the type specified by the `except`.\n",
+ "If they match, it executes the code in the `except` block."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`IOError` is the particular kind of exception Python raises\n",
+ "when there is a problem related to input and output,\n",
+ "such as files not existing\n",
+ "or the program not having the permissions it needs to read them.\n",
+ "We can put as many lines of code in a `try` block as we want,\n",
+ "just as we can put many statements under an `if`.\n",
+ "We can also handle several different kinds of errors afterward.\n",
+ "For example,\n",
+ "here's some code to calculate the entropy at each point in a grid:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "try:\n",
+ " params = read_params(param_file)\n",
+ " grid = read_grid(grid_file)\n",
+ " entropy = lee_entropy(params, grid)\n",
+ " write_entropy(entropy_file, entropy)\n",
+ "except IOError:\n",
+ " log_error_and_exit('IO error')\n",
+ "except ArithmeticError:\n",
+ " log_error_and_exit('Arithmetic error')\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Python tries to run the four functions inside the `try` as normal.\n",
+ "If an error occurs in any of them,\n",
+ "Python immediately jumps down\n",
+ "and tries to find an `except` of the corresponding type:\n",
+ "if the exception is an `IOError`,\n",
+ "Python jumps into the first error handler,\n",
+ "while if it's an `ArithmeticError`,\n",
+ "Python jumps into the second handler instead.\n",
+ "It will only execute one of these,\n",
+ "just as it will only execute one branch\n",
+ "of a series of `if`/`elif`/`else` statements."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This layout has made the code easier to read,\n",
+ "but we've lost something important:\n",
+ "the message printed out by the `IOError` branch doesn't tell us\n",
+ "which file caused the problem.\n",
+ "We can do better if we capture and hang on to the object that Python creates\n",
+ "to record information about the error:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "try:\n",
+ " params = read_params(param_file)\n",
+ " grid = read_grid(grid_file)\n",
+ " entropy = lee_entropy(params, grid)\n",
+ " write_entropy(entropy_file, entropy)\n",
+ "except IOError as err:\n",
+ " log_error_and_exit('Cannot read/write' + err.filename)\n",
+ "except ArithmeticError as err:\n",
+ " log_error_and_exit(err.message)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If something goes wrong in the `try`,\n",
+ "Python creates an exception object,\n",
+ "fills it with information,\n",
+ "and assigns it to the variable `err`.\n",
+ "(There's nothing special about this variable name—we can use anything we want.)\n",
+ "Exactly what information is recorded depends on what kind of error occurred;\n",
+ "Python's documentation describes the properties of each type of error in detail,\n",
+ "but we can always just print the exception object.\n",
+ "In the case of an I/O error,\n",
+ "we print out the name of the file that caused the problem.\n",
+ "And in the case of an arithmetic error,\n",
+ "printing out the message embedded in the exception object is what Python would have done anyway."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "So much for how exceptions work:\n",
+ "how should they be used?\n",
+ "Some programmers use `try` and `except` to give their programs default behaviors.\n",
+ "For example,\n",
+ "if this code can't read the grid file that the user has asked for,\n",
+ "it creates a default grid instead:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "try:\n",
+ " grid = read_grid(grid_file)\n",
+ "except IOError:\n",
+ " grid = default_grid()\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Other programmers would explicitly test for the grid file,\n",
+ "and use `if` and `else` for control flow:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "if file_exists(grid_file):\n",
+ " grid = read_grid(grid_file)\n",
+ "else:\n",
+ " grid = default_grid()\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It's mostly a matter of taste,\n",
+ "but we prefer the second style.\n",
+ "As a rule,\n",
+ "exceptions should only be used to handle exceptional cases.\n",
+ "If the program knows how to fall back to a default grid,\n",
+ "that's not an unexpected event.\n",
+ "Using `if` and `else`\n",
+ "instead of `try` and `except`\n",
+ "sends different signals to anyone reading our code,\n",
+ "even if they do the same thing."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Novices often ask another question about exception handling style as well,\n",
+ "but before we address it,\n",
+ "there's something in our example that you might not have noticed.\n",
+ "Exceptions can actually be thrown a long way:\n",
+ "they don't have to be handled immediately.\n",
+ "Take another look at this code:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "try:\n",
+ " params = read_params(param_file)\n",
+ " grid = read_grid(grid_file)\n",
+ " entropy = lee_entropy(params, grid)\n",
+ " write_entropy(entropy_file, entropy)\n",
+ "except IOError as err:\n",
+ " log_error_and_exit('Cannot read/write' + err.filename)\n",
+ "except ArithmeticError as err:\n",
+ " log_error_and_exit(err.message)\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The four lines in the `try` block are all function calls.\n",
+ "They might catch and handle exceptions themselves,\n",
+ "but if an exception occurs in one of them that *isn't* handled internally,\n",
+ "Python looks in the calling code for a matching `except`.\n",
+ "If it doesn't find one there,\n",
+ "it looks in that function's caller,\n",
+ "and so on.\n",
+ "If we get all the way back to the main program without finding an exception handler,\n",
+ "Python's default behavior is to print an error message like the ones we've been seeing all along."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This rule is the origin of the rule \"Throw Low, Catch High.\"\n",
+ "There are many places in our program where an error might occur.\n",
+ "There are only a few, though, where errors can sensibly be handled.\n",
+ "For example,\n",
+ "a linear algebra library doesn't know whether it's being called directly from the Python interpreter,\n",
+ "or whether it's being used as a component in a larger program.\n",
+ "In the latter case,\n",
+ "the library doesn't know if the program that's calling it is being run from the command line or from a GUI.\n",
+ "The library therefore shouldn't try to handle or report errors itself,\n",
+ "because it has no way of knowing what the right way to do this is.\n",
+ "It should instead just raise an exception,\n",
+ "and let its caller figure out how best to handle it."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally,\n",
+ "we can raise exceptions ourselves if we want to.\n",
+ "In fact,\n",
+ "we *should* do this,\n",
+ "since it's the standard way in Python to signal that something has gone wrong.\n",
+ "Here,\n",
+ "for example,\n",
+ "is a function that reads a grid and checks its consistency:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "```python\n",
+ "def read_grid(grid_file):\n",
+ " '''Read grid, checking consistency.'''\n",
+ "\n",
+ " data = read_raw_data(grid_file)\n",
+ " if not grid_consistent(data):\n",
+ " raise Exception('Inconsistent grid: ' + grid_file)\n",
+ " result = normalize_grid(data)\n",
+ "\n",
+ " return result\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `raise` statement creates a new exception with a meaningful error message.\n",
+ "Since `read_grid` itself doesn't contain a `try`/`except` block,\n",
+ "this exception will always be thrown up and out of the function,\n",
+ "to be caught and handled by whoever is calling `read_grid`.\n",
+ "We can define new types of exceptions if we want to.\n",
+ "And we should,\n",
+ "so that errors in our code can be distinguished from errors in other people's code.\n",
+ "However,\n",
+ "this involves classes and objects,\n",
+ "which is outside the scope of these lessons."
+ ]
+ },
+ {
+ "cell_type": "heading",
+ "level": 2,
+ "metadata": {},
+ "source": [
+ "Key Points"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- Use `assert` to embed pre-conditions, post-conditions, and invariants in programs.\n",
+ "- Use `raise` to signal an error, and `try`/`except` to handle errors.\n",
+ "- Throw low, catch high.\n",
+ "- Construct the most informative error message possible."
+ ]
+ }
+ ],
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file
"level": 1,
"metadata": {},
"source": [
- "Basic Programming Using Python: Testing"
+ "Basic Programming Using Python: Unit Testing"
]
},
{
"metadata": {},
"source": [
"* Explain why it is not practical to prove a program correct by testing it.\n",
- "* Distinguish between pre-conditions, post-conditions, and invariants.\n",
- "* Correctly raise and handle exceptions.\n",
- "* Explain why exceptions are a better way to handle errors than special return codes.\n",
"* Correctly write unit tests using an xUnit-style unit testing framework.\n",
- "* Name and explain the three types of results a test can produce.\n",
- "* Explain what test-driven development is, and use it to develop functions with well-specified behavior."
+ "* Name and explain the three types of results a test can produce."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We created, found, and fixed over half a dozen bugs\n",
- "in our [previous lesson](python-4-files-lists.ipynb).\n",
- "How can we be sure that others aren't still lurking in our code?\n",
- "It's not an idle worry:\n",
- "every year,\n",
- "programmers find errors in software that has been in use for years,\n",
- "and the number of papers that have been retracted\n",
- "because of computational mistakes\n",
- "is constantly growing."
+ "Like any other piece of experimental apparatus,\n",
+ "a complex program requires a much higher investment in testing than a simple one.\n",
+ "Putting it another way,\n",
+ "a small script that is only going to be used once,\n",
+ "to produce one figure,\n",
+ "probably doesn't need separate testing:\n",
+ "its output is either correct or not.\n",
+ "A linear algebra library that will be used by thousands of people\n",
+ "in twice that number of applications\n",
+ "over the course of a decade,\n",
+ "on the other hand,\n",
+ "definitely does."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "The short answer is that it's practically impossible to prove that a program will always do what it's supposed to.\n",
+ "Unfortunately,\n",
+ "it's practically impossible to prove that a program will always do what it's supposed to.\n",
"To see why,\n",
"consider a function that checks whether a character strings contains only the letters 'A', 'C', 'G', and 'T'.\n",
"These four tests clearly aren't sufficient:"
"source": [
"but no matter how many we have,\n",
"we can always write a function that passes them,\n",
- "but does the wrong thing in other cases."
+ "but does the wrong thing in other cases.\n",
+ "And as we add more tests,\n",
+ "we have to start worrying about whether the tests themselves are correct,\n",
+ "and about whether we can afford the time needed to write them.\n",
+ "After all,\n",
+ "if we really want to check that the square root function is correct for all values between 0.0 and 1.0,\n",
+ "we need to write over a billion test cases;\n",
+ "that's a lot of typing,\n",
+ "and the chances of us getting every one right are effectively zero."
]
},
{
"we can catch and fix errors while the changes are still fresh in our minds."
]
},
- {
- "cell_type": "heading",
- "level": 2,
- "metadata": {},
- "source": [
- "Defensive Programming"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The first step is to use [defensive programming](glossary.html#defensive_programming),\n",
- "i.e.,\n",
- "to put assertions in our programs so that they check their own execution as they run.\n",
- "Programs like the Firefox browser are littered with assertions:\n",
- "10-20% of the code they contain\n",
- "are there to check that the other 80-90% are working correctly.\n",
- "Broadly speaking,\n",
- "assertions fall into three categories:\n",
- "\n",
- "- A [precondition](glossary.html#precondition) is something that must be true\n",
- " in order for a piece of code to work correctly.\n",
- "- A [postcondition](glossary.html#postcondition) is something that must be true\n",
- " at the end of a piece of code if it worked correctly.\n",
- "- An [invariant](glossary.html#invariant) is something that is always true\n",
- " at a particular point inside a piece of code."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For example,\n",
- "suppose we are representing rectangles using a list of four coordinates `[x0, y0, x1, y1]`.\n",
- "In order to do some calculations,\n",
- "we need to normalize the rectangle so that it is at the origin\n",
- "and 1.0 units long on its longest axis.\n",
- "This function does that:"
- ]
- },
- {
- "cell_type": "code",
- "collapsed": false,
- "input": [
- "def normalize_rectangle(rect):\n",
- " x0, y0, x1, y1 = rect\n",
- " assert x0 < x1, 'Invalid X coordinates'\n",
- " assert y0 < y1, 'Invalid Y coordinates'\n",
- "\n",
- " dx = x1 - x0\n",
- " dy = y1 - y0\n",
- " if dx > dy:\n",
- " scaled = float(dy) / dx\n",
- " upper_x, upper_y = 1.0, scaled\n",
- " else:\n",
- " scaled = float(dx) / dy\n",
- " upper_x, upper_y = scaled, 1.0\n",
- "\n",
- " assert 0 < upper_x <= 1.0, 'Calculated upper X coordinate invalid'\n",
- " assert 0 < upper_y <= 1.0, 'Calculated upper Y coordinate invalid'\n",
- "\n",
- " return [0, 0, upper_x, upper_y]"
- ],
- "language": "python",
- "metadata": {},
- "outputs": [],
- "prompt_number": 7
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The first two assertions check that we've been given a legal rectangle,\n",
- "while the last two check the output we're about to return to our caller.\n",
- "Strictly speaking these post-conditions are redundant:\n",
- "if the inputs and calculations are correct,\n",
- "the last two assertions should always hold.\n",
- "But those are pretty big ifs,\n",
- "and having the program check itself can save us a lot of hunting around later."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "<hr/>\n",
- "### *Assertions and Bugs*\n",
- "\n",
- "<em>\n",
- "Another rule that good programmers follow is, \"Bugs become assertions.\"\n",
- "Whenever we fix a bug in a program,\n",
- "we should add some assertions to the program at that point to catch the bug if it reappears.\n",
- "After all,\n",
- "if we made the mistake once,\n",
- "then we (or someone else) might well make it again.\n",
- "Few things are as frustrating as\n",
- "having someone delete several carefully-crafted lines of code that fixed a subtle problem\n",
- "because they didn't realize what problem those lines were there to fix.\n",
- "</em>\n",
- "<hr/>"
- ]
- },
- {
- "cell_type": "heading",
- "level": 2,
- "metadata": {},
- "source": [
- "Handling Errors"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Even when programmers are careful,\n",
- "things can still go wrong.\n",
- "Some errors have external causes,\n",
- "like missing or badly-formatted files.\n",
- "Others are internal,\n",
- "like bugs in code.\n",
- "Either way,\n",
- "most modern programming languages handle errors in more or less the same way."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's start with a look at how people used to do error handling.\n",
- "Back in the Dark Ages,\n",
- "programmers wrote functions to return a [status code](glossary.html#status_code)\n",
- "to indicate whether they had run correctly or not.\n",
- "This led to programs like this:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "params, status = read_params(param_file)\n",
- "if status != OK:\n",
- " log.error('Failed to read', param_file)\n",
- " sys.exit(ERROR)\n",
- "\n",
- "grid, status = read_grid(grid_file)\n",
- "if status != OK:\n",
- " log.error('Failed to read', grid_file)\n",
- " sys.exit(ERROR)\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The two function calls are all we really want;\n",
- "the other six lines to check that files were opened and read properly,\n",
- "and to report errors and exit if not."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "A lot of code is still written this way,\n",
- "but this coding style makes it hard to see the forest for the trees.\n",
- "When we're reading a program,\n",
- "we want to understand what's supposed to happen when everything works,\n",
- "and only then think about what might happen if something goes wrong.\n",
- "When the two are interleaved,\n",
- "both are harder to understand.\n",
- "The net result is that most programmers don't bother to check the status codes their functions return."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "[Exceptions](glossary.html#exception) allow us to separate the \"normal\" flow of control\n",
- "from the \"exceptional\" cases that arise when something goes wrong.\n",
- "Using them produces code like this,\n",
- "which is much easier to understand:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "try:\n",
- " params = read_params(param_file)\n",
- " grid = read_grid(grid_file)\n",
- "except:\n",
- " log.error('Failed to read', filename)\n",
- " sys.exit(ERROR)\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We join the normal case and the error-handling code using the keywords `try` and `except`.\n",
- "These work together like `if` and `else`:\n",
- "the statements under the `try` are what should happen if everything works,\n",
- "while the statements under `except` are what the program should do if something goes wrong."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We have actually seen exceptions before without knowing it,\n",
- "since by default,\n",
- "when an exception occurs,\n",
- "Python prints it out and halts our program.\n",
- "For example,\n",
- "trying to open a nonexistent file triggers a type of exception called an `IOError`,\n",
- "while an out-of-bounds index to a list triggers an `IndexError`:"
- ]
- },
- {
- "cell_type": "code",
- "collapsed": false,
- "input": [
- "open('nonexistent-file.txt', 'r')"
- ],
- "language": "python",
- "metadata": {},
- "outputs": [
- {
- "ename": "IOError",
- "evalue": "[Errno 2] No such file or directory: 'nonexistent-file.txt'",
- "output_type": "pyerr",
- "traceback": [
- "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mIOError\u001b[0m Traceback (most recent call last)",
- "\u001b[0;32m<ipython-input-8-58cbde3dd63c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mopen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'nonexistent-file.txt'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'r'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
- "\u001b[0;31mIOError\u001b[0m: [Errno 2] No such file or directory: 'nonexistent-file.txt'"
- ]
- }
- ],
- "prompt_number": 8
- },
- {
- "cell_type": "code",
- "collapsed": false,
- "input": [
- "values = [0, 1, 2]\n",
- "print values[999]"
- ],
- "language": "python",
- "metadata": {},
- "outputs": [
- {
- "ename": "IndexError",
- "evalue": "list index out of range",
- "output_type": "pyerr",
- "traceback": [
- "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)",
- "\u001b[0;32m<ipython-input-9-7fed13afc650>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mprint\u001b[0m \u001b[0mvalues\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m999\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
- "\u001b[0;31mIndexError\u001b[0m: list index out of range"
- ]
- }
- ],
- "prompt_number": 9
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can use `try` and `except` to deal with these errors ourselves\n",
- "if we don't want the program simply to fall over.\n",
- "Here,\n",
- "for example,\n",
- "we put our attempt to open a nonexistent file inside a `try`,\n",
- "and in the `except`, we print a not-very-helpful error message:"
- ]
- },
- {
- "cell_type": "code",
- "collapsed": false,
- "input": [
- "try:\n",
- " reader = open('nonexistent-file.txt', 'r')\n",
- "except IOError:\n",
- " print 'Whoops!'"
- ],
- "language": "python",
- "metadata": {},
- "outputs": [
- {
- "output_type": "stream",
- "stream": "stdout",
- "text": [
- "Whoops!\n"
- ]
- }
- ],
- "prompt_number": 10
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "When Python executes this code,\n",
- "it runs the statement inside the `try`.\n",
- "If that works, it skips over the `except` block without running it.\n",
- "If an exception occurs inside the `try` block,\n",
- "though,\n",
- "Python compares the type of the exception to the type specified by the `except`.\n",
- "If they match, it executes the code in the `except` block."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "`IOError` is Python's way of reporting several kinds of problems\n",
- "related to input and output:\n",
- "not just files that don't exist,\n",
- "but also things like not having permission to read files,\n",
- "and so on.\n",
- "We can put as many lines of code in a `try` block as we want,\n",
- "just as we can put many statements under an `if`.\n",
- "We can also handle several different kinds of errors afterward.\n",
- "For example,\n",
- "here's some code to calculate the entropy at each point in a grid:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "try:\n",
- " params = read_params(param_file)\n",
- " grid = read_grid(grid_file)\n",
- " entropy = lee_entropy(params, grid)\n",
- " write_entropy(entropy_file, entropy)\n",
- "except IOError:\n",
- " log_error_and_exit('IO error')\n",
- "except ArithmeticError:\n",
- " log_error_and_exit('Arithmetic error')\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Python tries to run the four functions inside the `try` as normal.\n",
- "If an error occurs in any of them,\n",
- "Python immediately jumps down\n",
- "and tries to find an `except` of the corresponding type:\n",
- "if the exception is an `IOError`,\n",
- "Python jumps into the first error handler,\n",
- "while if it's an `ArithmeticError`,\n",
- "Python jumps into the second handler instead.\n",
- "It will only execute one of these,\n",
- "just as it will only execute one branch\n",
- "of a series of `if`/`elif`/`else` statements."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This layout has made the code easier to read,\n",
- "but we've lost something important:\n",
- "the message printed out by the `IOError` branch doesn't tell us\n",
- "which file caused the problem.\n",
- "We can do better if we capture and hang on to the object that Python creates\n",
- "to record information about the error:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "try:\n",
- " params = read_params(param_file)\n",
- " grid = read_grid(grid_file)\n",
- " entropy = lee_entropy(params, grid)\n",
- " write_entropy(entropy_file, entropy)\n",
- "except IOError as err:\n",
- " log_error_and_exit('Cannot read/write' + err.filename)\n",
- "except ArithmeticError as err:\n",
- " log_error_and_exit(err.message)\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "If something goes wrong in the `try`,\n",
- "Python creates an exception object,\n",
- "fills it with information,\n",
- "and assigns it to the variable `err`.\n",
- "(There's nothing special about this variable name—we can use anything we want.)\n",
- "Exactly what information is recorded depends on what kind of error occurred;\n",
- "Python's documentation describes the properties of each type of error in detail,\n",
- "but we can always just print the exception object.\n",
- "In the case of an I/O error,\n",
- "we print out the name of the file that caused the problem.\n",
- "And in the case of an arithmetic error,\n",
- "printing out the message embedded in the exception object is what Python would have done anyway."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "So much for how exceptions work:\n",
- "how should they be used?\n",
- "Some programmers use `try` and `except` to give their programs default behaviors.\n",
- "For example,\n",
- "if this code can't read the grid file that the user has asked for,\n",
- "it creates a default grid instead:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "try:\n",
- " grid = read_grid(grid_file)\n",
- "except IOError:\n",
- " grid = default_grid()\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Other programmers would explicitly test for the grid file,\n",
- "and use `if` and `else` for control flow:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "if file_exists(grid_file):\n",
- " grid = read_grid(grid_file)\n",
- "else:\n",
- " grid = default_grid()\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "It's mostly a matter of taste,\n",
- "but we prefer the second style.\n",
- "As a rule,\n",
- "exceptions should only be used to handle exceptional cases.\n",
- "If the program knows how to fall back to a default grid,\n",
- "that's not an unexpected event.\n",
- "Using `if` and `else`\n",
- "instead of `try` and `except`\n",
- "sends different signals to anyone reading our code,\n",
- "even if they do the same thing."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Novices often ask another question about exception handling style as well,\n",
- "but before we address it,\n",
- "there's something in our example that you might not have noticed.\n",
- "Exceptions can actually be thrown a long way:\n",
- "they don't have to be handled immediately.\n",
- "Take another look at this code:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "try:\n",
- " params = read_params(param_file)\n",
- " grid = read_grid(grid_file)\n",
- " entropy = lee_entropy(params, grid)\n",
- " write_entropy(entropy_file, entropy)\n",
- "except IOError as err:\n",
- " log_error_and_exit('Cannot read/write' + err.filename)\n",
- "except ArithmeticError as err:\n",
- " log_error_and_exit(err.message)\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The four lines in the `try` block are all function calls.\n",
- "They might catch and handle exceptions themselves,\n",
- "but if an exception occurs in one of them that *isn't* handled internally,\n",
- "Python looks in the calling code for a matching `except`.\n",
- "If it doesn't find one there,\n",
- "it looks in that function's caller,\n",
- "and so on.\n",
- "If we get all the way back to the main program without finding an exception handler,\n",
- "Python's default behavior is to print an error message like the ones we've been seeing all along."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "This rule is the origin of the rule \"Throw Low, Catch High.\"\n",
- "There are many places in our program where an error might occur.\n",
- "There are only a few, though, where errors can sensibly be handled.\n",
- "For example,\n",
- "a linear algebra library doesn't know whether it's being called directly from the Python interpreter,\n",
- "or whether it's being used as a component in a larger program.\n",
- "In the latter case,\n",
- "the library doesn't know if the program that's calling it is being run from the command line or from a GUI.\n",
- "The library therefore shouldn't try to handle or report errors itself,\n",
- "because it has no way of knowing what the right way to do this is.\n",
- "It should instead just raise an exception,\n",
- "and let its caller figure out how best to handle it."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Finally,\n",
- "we can raise exceptions ourselves if we want to.\n",
- "In fact,\n",
- "we *should* do this,\n",
- "since it's the standard way in Python to signal that something has gone wrong.\n",
- "Here,\n",
- "for example,\n",
- "is a function that reads a grid and checks its consistency:"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "```python\n",
- "def read_grid(grid_file):\n",
- " '''Read grid, checking consistency.'''\n",
- "\n",
- " data = read_raw_data(grid_file)\n",
- " if not grid_consistent(data):\n",
- " raise Exception('Inconsistent grid: ' + grid_file)\n",
- " result = normalize_grid(data)\n",
- "\n",
- " return result\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The `raise` statement creates a new exception with a meaningful error message.\n",
- "Since `read_grid` itself doesn't contain a `try`/`except` block,\n",
- "this exception will always be thrown up and out of the function,\n",
- "to be caught and handled by whoever is calling `read_grid`.\n",
- "We can define new types of exceptions if we want to.\n",
- "And we should,\n",
- "so that errors in our code can be distinguished from errors in other people's code.\n",
- "However,\n",
- "this involves classes and objects,\n",
- "which is outside the scope of these lessons."
- ]
- },
{
"cell_type": "heading",
"level": 2,
"cell_type": "markdown",
"metadata": {},
"source": [
- "Now that we understand how Python manages error,\n",
- "we can return to the subject of testing.\n",
"Most people don't enjoy writing tests,\n",
"so if we want them to actually do it,\n",
"it must be easy to:\n",
"cell_type": "markdown",
"metadata": {},
"source": [
- "- Use `assert` to embed pre-conditions, post-conditions, and invariants in programs.\n",
- "- Use `raise` to signal an error, and `try`/`except` to handle errors.\n",
"- Use a unit-testing framework to check and re-check code's correctness.\n",
"- Put each unit test in its own small function.\n",
"- Use test-driven development to define how functions should behave."