From: Greg Wilson Date: Fri, 13 Sep 2013 18:13:35 +0000 (-0400) Subject: Splitting assertions from unit testing/TDD X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=ee30ecbda1db63efb757fd72ed9405185ecec19f;p=swc-testing-nose.git Splitting assertions from unit testing/TDD W. Trevor King: I dropped everything from the original c632882 except for the lessons/swc-python/ modifications. Conflicts: lessons/swc-python/tutorial.html --- diff --git a/lessons/swc-python/python-5-errors.ipynb b/lessons/swc-python/python-5-errors.ipynb new file mode 100644 index 0000000..d01b818 --- /dev/null +++ b/lessons/swc-python/python-5-errors.ipynb @@ -0,0 +1,599 @@ +{ + "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": [ + "
\n", + "### *Assertions and Bugs*\n", + "\n", + "\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", + "\n", + "
" + ] + }, + { + "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\u001b[0m in \u001b[0;36m\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\u001b[0m in \u001b[0;36m\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 diff --git a/lessons/swc-python/python-5-testing.ipynb b/lessons/swc-python/python-6-testing.ipynb similarity index 51% rename from lessons/swc-python/python-5-testing.ipynb rename to lessons/swc-python/python-6-testing.ipynb index fe176c3..7f9446c 100644 --- a/lessons/swc-python/python-5-testing.ipynb +++ b/lessons/swc-python/python-6-testing.ipynb @@ -12,7 +12,7 @@ "level": 1, "metadata": {}, "source": [ - "Basic Programming Using Python: Testing" + "Basic Programming Using Python: Unit Testing" ] }, { @@ -28,12 +28,8 @@ "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." ] }, { @@ -48,22 +44,26 @@ "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:" @@ -156,7 +156,15 @@ "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." ] }, { @@ -185,587 +193,6 @@ "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": [ - "
\n", - "### *Assertions and Bugs*\n", - "\n", - "\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", - "\n", - "
" - ] - }, - { - "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\u001b[0m in \u001b[0;36m\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\u001b[0m in \u001b[0;36m\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, @@ -778,8 +205,6 @@ "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", @@ -1269,8 +694,6 @@ "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."