{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "heading", "level": 2, "metadata": { "cell_tags": [] }, "source": [ "Defensive Programming" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Our previous lessons have introduced the basic tools of programming:\n", "variables and lists,\n", "file I/O,\n", "loops,\n", "conditionals,\n", "and functions.\n", "What they *haven't* done is show us how to tell\n", "whether a program is getting the right answer,\n", "and how to tell if it's *still* getting the right answer\n", "as we make changes to it.\n", "\n", "To achieve that,\n", "we need to:\n", "\n", "* write programs that check their own operation,\n", "* write and run tests for widely-used functions, and\n", "* make sure we know what \"correct\" actually means.\n", "\n", "The good news is,\n", "doing these things will speed up our programming,\n", "not slow it down.\n", "As in real carpentry—the kind done with lumber—the time saved\n", "by measuring carefully before cutting a piece of wood\n", "is much greater than the time that measuring takes." ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "#### Objectives\n", "\n", "* Explain what an assertion is.\n", "* Add assertions to programs that correctly check the program's state.\n", "* Correctly add precondition and postcondition assertions to functions.\n", "* Explain what test-driven development is, and use it when creating new functions.\n", "* Explain why variables should be initialized using actual data values rather than arbitrary constants.\n", "* Debug code containing an error systematically." ] }, { "cell_type": "heading", "level": 3, "metadata": { "cell_tags": [] }, "source": [ "Assertions" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "The first step toward getting the right answers from our programs\n", "is to assume that mistakes *will* happen\n", "and to guard against them.\n", "This is called [defensive programming](../../gloss.html#defensive-programming),\n", "and the most common way to do it is to add [assertions](../../gloss.html#assertion) to our code\n", "so that it checks itself as it runs.\n", "An assertion is simply a statement that something must be true at a certain point in a program.\n", "When Python sees one,\n", "it checks that the assertion's condition.\n", "If it's true,\n", "Python does nothing,\n", "but if it's false,\n", "Python halts the program immediately\n", "and prints the error message provided.\n", "For example,\n", "this piece of code halts as soon as the loop encounters a value that isn't positive:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "numbers = [1.5, 2.3, 0.7, -0.001, 4.4]\n", "total = 0.0\n", "for n in numbers:\n", " assert n >= 0.0, 'Data should only contain positive values'\n", " total += n\n", "print 'total is:', total" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "Data should only contain positive values", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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 2\u001b[0m \u001b[0mtotal\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0.0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mn\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mnumbers\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mn\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Data should only contain positive values'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0mtotal\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32mprint\u001b[0m \u001b[0;34m'total is:'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtotal\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Data should only contain positive values" ] } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Programs like the Firefox browser are full of 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](../../gloss.html#precondition) is something that must be true\n", " at the start of a function in order for it to work correctly.\n", "- A [postcondition](../../gloss.html#postcondition) is something that\n", " the function guarantees is true when it finishes.\n", "- An [invariant](../../gloss.html#invariant) is something that is always true\n", " at a particular point inside a piece of code.\n", "\n", "For example,\n", "suppose we are representing rectangles using a tuple 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,\n", "but checks that its input is correctly formatted and that its result makes sense:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def normalize_rectangle(rect):\n", " '''Normalizes a rectangle so that it is at the origin and 1.0 units long on its longest axis.'''\n", " assert len(rect) == 4, 'Rectangles must contain 4 coordinates'\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(dx) / dy\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": { "cell_tags": [] }, "outputs": [], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "The preconditions on lines 2, 4, and 5 catch invalid inputs:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print normalize_rectangle( (0.0, 1.0, 2.0) ) # missing the fourth coordinate" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "Rectangles must contain 4 coordinates", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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[0;32mprint\u001b[0m \u001b[0mnormalize_rectangle\u001b[0m\u001b[0;34m(\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m)\u001b[0m \u001b[0;31m# missing the fourth coordinate\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mnormalize_rectangle\u001b[0;34m(rect)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnormalize_rectangle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrect\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m'''Normalizes a rectangle so that it is at the origin and 1.0 units long on its longest axis.'''\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrect\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Rectangles must contain 4 coordinates'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mx0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrect\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mx0\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Invalid X coordinates'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Rectangles must contain 4 coordinates" ] } ], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "print normalize_rectangle( (4.0, 2.0, 1.0, 5.0) ) # X axis inverted" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "Invalid X coordinates", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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[0;32mprint\u001b[0m \u001b[0mnormalize_rectangle\u001b[0m\u001b[0;34m(\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m4.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m5.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m)\u001b[0m \u001b[0;31m# X axis inverted\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mnormalize_rectangle\u001b[0;34m(rect)\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrect\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Rectangles must contain 4 coordinates'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mx0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrect\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mx0\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mx1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Invalid X coordinates'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0my0\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0my1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Invalid Y coordinates'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Invalid X coordinates" ] } ], "prompt_number": 6 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "The post-conditions help us catch bugs by telling us when our calculations cannot have been correct.\n", "For example,\n", "if we normalize a rectangle that is taller than it is wide everything seems OK:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print normalize_rectangle( (0.0, 0.0, 1.0, 5.0) )" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "(0, 0, 0.2, 1.0)\n" ] } ], "prompt_number": 7 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "but if we normalize one that's wider than it is tall,\n", "the assertion is triggered:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print normalize_rectangle( (0.0, 0.0, 5.0, 1.0) )" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "Calculated upper Y coordinate invalid", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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[0;32mprint\u001b[0m \u001b[0mnormalize_rectangle\u001b[0m\u001b[0;34m(\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m5.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mnormalize_rectangle\u001b[0;34m(rect)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0;36m0\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mupper_x\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Calculated upper X coordinate invalid'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0;36m0\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mupper_y\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'Calculated upper Y coordinate invalid'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mupper_x\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mupper_y\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: Calculated upper Y coordinate invalid" ] } ], "prompt_number": 8 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Re-reading our function,\n", "we realize that line 10 should divide `dy` by `dx` rather than `dx` by `dy`.\n", "(You can display line numbers by typing Ctrl-M, then L.)\n", "If we had left out the assertion at the end of the function,\n", "we would have created and returned something that had the right shape as a valid answer,\n", "but wasn't.\n", "Detecting and debugging that would almost certainly have taken more time in the long run\n", "than writing the assertion.\n", "\n", "But assertions aren't just about catching errors:\n", "they also help people understand programs.\n", "Each assertion gives the person reading the program\n", "a chance to check (consciously or otherwise)\n", "that their understanding matches what the code is doing.\n", "\n", "Most good programmers follow two rules when adding assertions to their code.\n", "The first is, \"[fail early, fail often](../../rules.html#fail-early-fail-often)\".\n", "The greater the distance between when and where an error occurs and when it's noticed,\n", "the harder the error will be to debug,\n", "so good code catches mistakes as early as possible.\n", "\n", "The second rule is, \"[turn bugs into assertions or tests](../../rules.html#turn-bugs-into-assertions-or-tests)\".\n", "If you made a mistake in a piece of code,\n", "the odds are good that you have made other mistakes nearby,\n", "or will make the same mistake (or a related one)\n", "the next time you change it.\n", "Writing assertions to check that you haven't [regressed](../../gloss.html#regression)\n", "(i.e., haven't re-introduced an old problem)\n", "can save a lot of time in the long run,\n", "and helps to warn people who are reading the code\n", "(including your future self)\n", "that this bit is tricky." ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "#### Challenges\n", "\n", "1. Suppose you are writing a function called `average` that calculates the average of the numbers in a list.\n", " What pre-conditions and post-conditions would you write for it?\n", " Compare your answer to your neighbor's:\n", " can you think of a function that will past your tests but not hers or vice versa?\n", "\n", "2. Explain in words what the assertions in this code check,\n", " and for each one,\n", " give an example of input that will make that assertion fail.\n", " \n", " ~~~\n", " def running(values):\n", " assert len(values) > 0\n", " result = [values[0]]\n", " for v in values[1:]:\n", " assert result[-1] >= 0\n", " result.append(result[-1] + v)\n", " assert result[-1] >= result[0]\n", " return result\n", " ~~~" ] }, { "cell_type": "heading", "level": 3, "metadata": { "cell_tags": [] }, "source": [ "Test-Driven Development" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "An assertion checks that something is true at a particular point in the program.\n", "The next step is to check the overall behavior of a piece of code,\n", "i.e.,\n", "to make sure that it produces the right output when it's given a particular input.\n", "For example,\n", "suppose we need to find where two or more time series overlap.\n", "The range of each time series is represented as a pair of numbers,\n", "which are the time the interval started and ended.\n", "The output is the largest range that they all include:" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "\"Overlapping" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Most novice programmers would solve this problem like this:\n", "\n", "1. Write a function `range_overlap`.\n", "2. Call it interactively on two or three different inputs.\n", "3. If it produces the wrong answer, fix the function and re-run that test.\n", "\n", "This clearly works—after all, thousands of scientists are doing it right now—but\n", "there's a better way:\n", "\n", "1. Write a short function for each test.\n", "2. Write a `range_overlap` function that should pass those tests.\n", "3. If `range_overlap` produces any wrong answers, fix it and re-run the test functions.\n", "\n", "Writing the tests *before* writing the function they exercise\n", "is called [test-driven development](../../gloss.html#test-driven-development) (TDD).\n", "Its advocates believe it produces better code faster because:\n", "\n", "1. If people write tests after writing the thing to be tested,\n", " they are subject to confirmation bias,\n", " i.e.,\n", " they subconsciously write tests to show that their code is correct,\n", " rather than to find errors.\n", "2. Writing tests helps programmers figure out what the function is actually supposed to do.\n", "\n", "Here are three test functions for `range_overlap`:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0)\n", "assert range_overlap([ (0.0, 1.0), (0.0, 2.0) ]) == (0.0, 1.0)\n", "assert range_overlap([ (0.0, 1.0), (0.0, 2.0), (-1.0, 1.0) ]) == (0.0, 1.0)" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [], "prompt_number": 9 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "The error is actually reassuring:\n", "we haven't written `range_overlap` yet,\n", "so if the tests passed,\n", "it would be a sign that someone else had\n", "and that we were accidentally using their function.\n", "\n", "And as a bonus of writing these tests,\n", "we've implicitly defined what our input and output look like:\n", "we expect a list of pairs as input,\n", "and produce a single pair as output.\n", "\n", "Something important is missing, though.\n", "We don't have any tests for the case where the ranges don't overlap at all:\n", "\n", "~~~\n", "assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == ???\n", "~~~\n", "\n", "What should `range_overlap` do in this case:\n", "fail with an error message,\n", "produce a special value like `(0.0, 0.0)` to signal that there's no overlap,\n", "or something else?\n", "Any actual implementation of the function will do one of these things;\n", "writing the tests first helps us figure out which is best\n", "*before* we're emotionally invested in whatever we happened to write\n", "before we realized there was an issue.\n", "\n", "And what about this case?\n", "\n", "~~~\n", "assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == ???\n", "~~~\n", "\n", "Do two segments that touch at their endpoints overlap or not?\n", "Mathematicians usually say \"yes\",\n", "but engineers usually say \"no\".\n", "The best answer is \"whatever is most useful in the rest of our program\",\n", "but again,\n", "any actual implementation of `range_overlap` is going to do *something*,\n", "and whatever it is ought to be consistent with what it does when there's no overlap at all.\n", "\n", "Since we're planning to use the range this function returns\n", "as the X axis in a time series chart,\n", "we decide that:\n", "\n", "1. every overlap has to have non-zero width, and\n", "2. we will return the special value `None` when there's no overlap.\n", "\n", "`None` is built into Python,\n", "and means \"nothing here\".\n", "(Other languages often call the equivalent value `null` or `nil`).\n", "With that decision made,\n", "we can finish writing our last two tests:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None\n", "assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m5.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m6.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "prompt_number": 10 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Again,\n", "we get an error because we haven't written our function,\n", "but we're now ready to do so:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def range_overlap(ranges):\n", " '''Return common overlap among a set of [low, high] ranges.'''\n", " lowest = 0.0\n", " highest = 1.0\n", " for (low, high) in ranges:\n", " lowest = max(lowest, low)\n", " highest = min(highest, high)\n", " return (lowest, highest)" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [], "prompt_number": 11 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "(Take a moment to think about why we use `max` to raise `lowest`\n", "and `min` to lower `highest`.)\n", "We'd now like to re-run our tests,\n", "but they're scattered across three different cells.\n", "To make running them easier,\n", "let's put them all in a function:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def test_range_overlap():\n", " assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0)\n", " assert range_overlap([ (0.0, 1.0), (0.0, 2.0) ]) == (0.0, 1.0)\n", " assert range_overlap([ (0.0, 1.0), (0.0, 2.0), (-1.0, 1.0) ]) == (0.0, 1.0)\n", " assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None\n", " assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [], "prompt_number": 12 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "We can now test `range_overlap` with a single function call:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "test_range_overlap()" ], "language": "python", "metadata": { "cell_tags": [] }, "outputs": [ { "ename": "AssertionError", "evalue": "", "output_type": "pyerr", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m\n\u001b[0;31mAssertionError\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[0mtest_range_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mtest_range_overlap\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m5.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m6.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mrange_overlap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "prompt_number": 13 }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "The first of the tests that was supposed to produce `None` fails,\n", "so we know there's something wrong with our function.\n", "What we *don't* know,\n", "though,\n", "is whether the last of our five tests passed or failed,\n", "because Python halted the program as soon as it spotted the first error.\n", "Still,\n", "some information is better than none,\n", "and if we trace the behavior of the function with that input,\n", "we realize that we're initializing `lowest` and `highest` to 0.0 and 1.0 respectively,\n", "regardless of the input values.\n", "This violates another important rule of programming:\n", "\"[always initialize from data](../../rules.html#always-initialize-from-data)\".\n", "We'll leave it as an exercise to fix `range_overlap`." ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [ "challenges" ] }, "source": [ "#### Challenges\n", "\n", "1. Fix `range_overlap`. Re-run `test_range_overlap` after each change you make." ] }, { "cell_type": "heading", "level": 3, "metadata": { "cell_tags": [] }, "source": [ "Debugging" ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "Once testing has uncovered problems,\n", "the next step is to fix them.\n", "Many novices do this by making more-or-less random changes to their code\n", "until it seems to produce the right answer,\n", "but that's very inefficient\n", "(and the result is usually only correct for the one case they're testing).\n", "The more experienced a programmer is,\n", "the more systematically they debug,\n", "and most follow some variation on the rules explained below.\n", "\n", "#### Know What It's Supposed to Do\n", "\n", "The first step in debugging something is to\n", "[know what it's supposed to do](../../rules.html#know-what-its-supposed-to-do).\n", "\"My program doesn't work\" isn't good enough:\n", "in order to diagnose and fix problems,\n", "we need to be able to tell correct output from incorrect.\n", "If we can write a test case for the failing case—i.e.,\n", "if we can assert that with *these* inputs,\n", "the function should produce *that* result—\n", "then we're ready to start debugging.\n", "If we can't,\n", "then we need to figure out how we're going to know when we've fixed things.\n", "\n", "But writing test cases for scientific software is frequently harder than\n", "writing test cases for commercial applications,\n", "because if we knew what the output of the scientific code was supposed to be,\n", "we wouldn't be running the software:\n", "we'd be writing up our results and moving on to the next program.\n", "In practice,\n", "scientists tend to do the following:\n", "\n", "1. *Test with simplified data.*\n", " Before doing statistics on a real data set,\n", " we should try calculating statistics for a single record,\n", " for two identical records,\n", " for two records whose values are one step apart,\n", " or for some other case where we can calculate the right answer by hand.\n", "\n", "2. *Test a simplified case.*\n", " If our program is supposed to simulate\n", " magnetic eddies in rapidly-rotating blobs of supercooled helium,\n", " our first test should be a blob of helium that isn't rotating,\n", " and isn't being subjected to any external electromagnetic fields.\n", " Similarly,\n", " if we're looking at the effects of climate change on speciation,\n", " our first test should hold temperature, precipitation, and other factors constant.\n", "\n", "3. *Compare to an oracle.*\n", " A [test oracle](../../gloss.html#test-oracle) is something—experimental data,\n", " an older program whose results are trusted,\n", " or even a human expert—against which we can compare the results of our new program.\n", " If we have a test oracle,\n", " we should store its output for particular cases\n", " so that we can compare it with our new results as often as we like\n", " without re-running that program.\n", "\n", "4. *Check conservation laws.*\n", " Mass, energy, and other quantitites are conserved in physical systems,\n", " so they should be in programs as well.\n", " Similarly,\n", " if we are analyzing patient data,\n", " the number of records should either stay the same or decrease\n", " as we move from one analysis to the next\n", " (since we might throw away outliers or records with missing values).\n", " If \"new\" patients start appearing out of nowhere as we move through our pipeline,\n", " it's probably a sign that something is wrong.\n", "\n", "5. *Visualize.*\n", " Data analysts frequently use simple visualizations to check both\n", " the science they're doing\n", " and the correctness of their code\n", " (just as we did in the [opening lesson](01-numpy.html) of this tutorial).\n", " This should only be used for debugging as a last resort,\n", " though,\n", " since it's very hard to compare two visualizations automatically.\n", "\n", "#### Make It Fail Every Time\n", "\n", "We can only debug something when it fails,\n", "so the second step is always to find a test case that\n", "[makes it fail every time](../../rules.html#make-it-fail-every-time).\n", "The \"every time\" part is important because\n", "few things are more frustrating than debugging an intermittent problem:\n", "if we have to call a function a dozen times to get a single failure,\n", "the odds are good that we'll scroll past the failure when it actually occurs.\n", "\n", "As part of this,\n", "it's always important to check that our code is \"plugged in\",\n", "i.e.,\n", "that we're actually exercising the problem that we think we are.\n", "Every programmer has spent hours chasing a bug,\n", "only to realize that they were actually calling their code on the wrong data set\n", "or with the wrong configuration parameters,\n", "or are using the wrong version of the software entirely.\n", "Mistakes like these are particularly likely to happen when we're tired,\n", "frustrated,\n", "and up against a deadline,\n", "which is one of the reasons late-night (or overnight) coding sessions\n", "are almost never worthwhile.\n", "\n", "#### Make It Fail Fast\n", "\n", "If it takes 20 minutes for the bug to surface,\n", "we can only do three experiments an hour.\n", "That doesn't must mean we'll get less data in more time:\n", "we're also more likely to be distracted by other things as we wait for our program to fail,\n", "which means the time we *are* spending on the problem is less focused.\n", "It's therefore critical to [make it fail fast](../../rules.html#make-it-fail-fast).\n", "\n", "As well as making the program fail fast in time,\n", "we want to make it fail fast in space,\n", "i.e.,\n", "we want to localize the failure to the smallest possible region of code:\n", "\n", "1. The smaller the gap between cause and effect,\n", " the easier the connection is to find.\n", " Many programmers therefore use a divide and conquer strategy to find bugs,\n", " i.e.,\n", " if the output of a function is wrong,\n", " they check whether things are OK in the middle,\n", " then concentrate on either the first or second half,\n", " and so on.\n", "\n", "2. N things can interact in N2/2 different ways,\n", " so every line of code that *isn't* run as part of a test\n", " means more than one thing we don't need to worry about.\n", "\n", "#### Change One Thing at a Time, For a Reason\n", "\n", "Replacing random chunks of code is unlikely to do much good.\n", "(After all,\n", "if you got it wrong the first time,\n", "you'll probably get it wrong the second and third as well.)\n", "Good programmers therefore\n", "[change one thing at a time, for a reason](../../rules.html#change-one-thing-at-a-time)\n", "They are either trying to gather more information\n", "(\"is the bug still there if we change the order of the loops?\")\n", "or test a fix\n", "(\"can we make the bug go away by sorting our data before processing it?\").\n", " \n", "Every time we make a change,\n", "however small,\n", "we should re-run our tests immediately,\n", "because the more things we change at once,\n", "the harder it is to know what's responsible for what\n", "(those N2 interactions again).\n", "And we should re-run *all* of our tests:\n", "more than half of fixes made to code introduce (or re-introduce) bugs,\n", "so re-running all of our tests tells us whether we have [regressed](../../gloss.html#regression).\n", "\n", "#### Keep Track of What You've Done\n", "\n", "Good scientists keep track of what they've done\n", "so that they can reproduce their work,\n", "and so that they don't waste time repeating the same experiments\n", "or running ones whose results won't be interesting.\n", "Similarly,\n", "debugging works best when we\n", "[keep track of what we've done](../../rules.html#keep-track-of-what-youve-done)\n", "and how well it worked.\n", "If we find ourselves asking,\n", "\"Did left followed by right with an odd number of lines cause the crash?\n", "Or was it right followed by left?\n", "Or was I using an even number of lines?\"\n", "then it's time to step away from the computer,\n", "take a deep breath,\n", "and start working more systematically.\n", " \n", "Records are particularly useful when the time comes to ask for help.\n", "People are more likely to listen to us\n", "when we can explain clearly what we did,\n", "and we're better able to give them the information they need to be useful.\n", "\n", "> #### Version Control Revisited\n", ">\n", "> Version control is often used to reset software to a known state during debugging,\n", "> and to explore recent changes to code that might be responsible for bugs.\n", "> In particular,\n", "> most version control systems have a `blame` command\n", "> that will show who last changed particular lines of code...\n", "\n", "#### Be Humble\n", "\n", "And speaking of help:\n", "if we can't find a bug in 10 minutes,\n", "we should [be humble](../../rules.html#be-humble) and ask for help.\n", "Just explaining the problem aloud is often useful,\n", "since hearing what we're thinking helps us spot inconsistencies and hidden assumptions.\n", "\n", "Asking for help also helps alleviate confirmation bias.\n", "If we have just spent an hour writing a complicated program,\n", "we want it to work,\n", "so we're likely to keep telling ourselves why it should,\n", "rather than searching for the reason it doesn't.\n", "People who aren't emotionally invested in the code can be more objective,\n", "which is why they're often able to spot the simple mistakes we have overlooked.\n", "\n", "Part of being humble is learning from our mistakes.\n", "Programmers tend to get the same things wrong over and over:\n", "either they don't understand the language and libraries they're working with,\n", "or their model of how things work is wrong.\n", "In either case,\n", "taking note of why the error occurred\n", "and checking for it next time\n", "quickly turns into not making the mistake at all.\n", "\n", "And that is what makes us most productive in the long run.\n", "As the saying goes,\n", "\"[A week of hard work can sometimes save you an hour of thought](../../rules.html#week-hard-work-hour-thought).\"\n", "If we train ourselves to avoid making some kinds of mistakes,\n", "to break our code into modular, testable chunks,\n", "and to turn every assumption (or mistake) into an assertion,\n", "it will actually take us *less* time to produce working programs,\n", "not more." ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [ "keypoints" ] }, "source": [ "#### Key Points\n", "\n", "* Program defensively, i.e., assume that errors are going to arise, and write code to detect them when they do.\n", "* Put assertions in programs to check their state as they run, and to help readers understand how those programs are supposed to work.\n", "* Use preconditions to check that the inputs to a function are safe to use.\n", "* Use postconditions to check that the output from a function is safe to use.\n", "* Write tests before writing code in order to help determine exactly what that code is supposed to do.\n", "* Know what code is supposed to do *before* trying to debug it.\n", "* Make it fail every time.\n", "* Make it fail fast.\n", "* Change one thing at a time, and for a reason.\n", "* Keep track of what you've done.\n", "* Be humble." ] }, { "cell_type": "markdown", "metadata": { "cell_tags": [] }, "source": [ "#### Next Steps\n", "\n", "We have now seen the basics of building and testing Python code in the IPython Notebook.\n", "The last thing we need to learn is how to build command-line programs\n", "that we can use in pipelines and shell scripts,\n", "so that we can integrate our tools with other people's work.\n", "This will be the subject of our next and final lesson." ] } ], "metadata": {} } ] }