{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Exceptions and Testing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Things go wrong when programming all the time. Some of these \"problems\" are errors that stop the program from making sense. Others are problems that stop the program from working in specific, special cases. These \"problems\" may be real, or we may want to treat them as special cases that don't stop the program from running.\n", "\n", "These special cases can be dealt with using *exceptions*.\n", "\n", "# Exceptions\n", "\n", "Let's define a function that divides two numbers." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from __future__ import division" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def divide(numerator, denominator):\n", " \"\"\"\n", " Divide two numbers.\n", " \n", " Parameters\n", " ----------\n", " \n", " numerator: float\n", " numerator\n", " denominator: float\n", " denominator\n", " \n", " Returns\n", " -------\n", " \n", " fraction: float\n", " numerator / denominator \n", " \"\"\"\n", " return numerator / denominator" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.8\n" ] } ], "source": [ "print(divide(4.0, 5.0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But what happens if we try something *really stupid*?" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "float division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\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[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdivide\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m4.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0.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;36mdivide\u001b[0;34m(numerator, denominator)\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \"\"\"\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m: float division by zero" ] } ], "source": [ "print(divide(4.0, 0.0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, the code works fine until we pass in input that we shouldn't. When we do, this causes the code to stop. To show how this can be a problem, consider the loop:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4.0\n" ] }, { "ename": "ZeroDivisionError", "evalue": "float division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\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[0mdenominators\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;36m0.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3.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[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdenominator\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdenominators\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdivide\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m4.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdenominator\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;36mdivide\u001b[0;34m(numerator, denominator)\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \"\"\"\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m: float division by zero" ] } ], "source": [ "denominators = [1.0, 0.0, 3.0, 5.0]\n", "for denominator in denominators:\n", " print(divide(4.0, denominator))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are three sensible results, but we only get the first." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "There are many more complex, real cases where it's not obvious that we're doing something wrong ahead of time. In this case, we want to be able to *try* running the code and *catch* errors without stopping the code. This can be done in Python:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dividing by zero is a silly thing to do!\n" ] } ], "source": [ "try:\n", " print(divide(4.0, 0.0))\n", "except ZeroDivisionError:\n", " print(\"Dividing by zero is a silly thing to do!\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4.0\n", "Dividing by zero is a silly thing to do!\n", "1.3333333333333333\n", "0.8\n" ] } ], "source": [ "denominators = [1.0, 0.0, 3.0, 5.0]\n", "for denominator in denominators:\n", " try:\n", " print(divide(4.0, denominator))\n", " except ZeroDivisionError:\n", " print(\"Dividing by zero is a silly thing to do!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The idea here is given by the names. Python will *try* to execute the code inside the `try` block. This is just like an `if` or a `for` block: each command that is indented in that block will be executed in order.\n", "\n", "If, and only if, an error arises then the `except` block will be checked. If the error that is produced matches the one listed then instead of stopping, the code inside the `except` block will be run instead.\n", "\n", "To show how this works with different errors, consider a different silly error:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for /: 'float' and 'str'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\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[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdivide\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m4.0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"zero\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mZeroDivisionError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Dividing by zero is a silly thing to do!\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36mdivide\u001b[0;34m(numerator, denominator)\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \"\"\"\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mdenominator\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for /: 'float' and 'str'" ] } ], "source": [ "try:\n", " print(divide(4.0, \"zero\"))\n", "except ZeroDivisionError:\n", " print(\"Dividing by zero is a silly thing to do!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that, as it makes no sense to divide by a string, we get a `TypeError` instead of a `ZeroDivisionError`. We could catch both errors:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dividing by a string is a silly thing to do!\n" ] } ], "source": [ "try:\n", " print(divide(4.0, \"zero\"))\n", "except ZeroDivisionError:\n", " print(\"Dividing by zero is a silly thing to do!\")\n", "except TypeError:\n", " print(\"Dividing by a string is a silly thing to do!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We could catch *any* error:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Some error occured\n" ] } ], "source": [ "try:\n", " print(divide(4.0, \"zero\"))\n", "except:\n", " print(\"Some error occured\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This doesn't give us much information, and may lose information that we need in order to handle the error. We can capture the exception to a variable, and then use that variable:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Some error occured: unsupported operand type(s) for /: 'float' and 'str'\n" ] } ], "source": [ "try:\n", " print(divide(4.0, \"zero\"))\n", "except (ZeroDivisionError, TypeError) as exception:\n", " print(\"Some error occured: {}\".format(exception))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we have caught two possible types of error within the tuple (which *must*, in this case, have parantheses) and captured the specific error in the variable `exception`. This variable can then be used: here we just print it out.\n", "\n", "Normally best practise is to be as specific as possible on the error you are trying to catch.\n", "\n", "## Extending the logic\n", "\n", "Sometimes you may want to perform an action *only* if an error did not occur. For example, let's suppose we wanted to store the result of dividing 4 by a divisor, and also store the divisor, but *only* if the divisor is valid.\n", "\n", "One way of doing this would be the following:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Error of type float division by zero for denominator 0.0\n", "Error of type unsupported operand type(s) for /: 'float' and 'str' for denominator zero\n", "[4.0, 1.3333333333333333, 0.8]\n", "[1.0, 3.0, 5.0]\n" ] } ], "source": [ "denominators = [1.0, 0.0, 3.0, \"zero\", 5.0]\n", "results = []\n", "divisors = []\n", "for denominator in denominators:\n", " try:\n", " result = divide(4.0, denominator)\n", " except (ZeroDivisionError, TypeError) as exception:\n", " print(\"Error of type {} for denominator {}\".format(exception, denominator))\n", " else:\n", " results.append(result)\n", " divisors.append(denominator)\n", "print(results)\n", "print(divisors)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The statements in the `else` block are only run if the `try` block succeeds. If it doesn't - if the statements in the `try` block raise an exception - then the statements in the `else` block are not run.\n", "\n", "## Exceptions in your own code\n", "\n", "Sometimes you don't want to wait for the code to break at a low level, but instead stop when you know things are going to go wrong. This is usually because you can be more informative about what's going wrong. Here's a slightly artificial example:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def divide_sum(numerator, denominator1, denominator2):\n", " \"\"\"\n", " Divide a number by a sum.\n", " \n", " Parameters\n", " ----------\n", " \n", " numerator: float\n", " numerator\n", " denominator1: float\n", " Part of the denominator\n", " denominator2: float\n", " Part of the denominator\n", " \n", " Returns\n", " -------\n", " \n", " fraction: float\n", " numerator / (denominator1 + denominator2)\n", " \"\"\"\n", " \n", " return numerator / (denominator1 + denominator2)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false }, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\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[0mdivide_sum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\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;36mdivide_sum\u001b[0;34m(numerator, denominator1, denominator2)\u001b[0m\n\u001b[1;32m 20\u001b[0m \"\"\"\n\u001b[1;32m 21\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 22\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdenominator1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mdenominator2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" ] } ], "source": [ "divide_sum(1, 1, -1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It should be obvious to the code that this is going to go wrong. Rather than letting the code hit the `ZeroDivisionError` exception automatically, we can *raise* it ourselves, with a more meaningful error message:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def divide_sum(numerator, denominator1, denominator2):\n", " \"\"\"\n", " Divide a number by a sum.\n", " \n", " Parameters\n", " ----------\n", " \n", " numerator: float\n", " numerator\n", " denominator1: float\n", " Part of the denominator\n", " denominator2: float\n", " Part of the denominator\n", " \n", " Returns\n", " -------\n", " \n", " fraction: float\n", " numerator / (denominator1 + denominator2)\n", " \"\"\"\n", " \n", " if (denominator1 + denominator2) == 0:\n", " raise ZeroDivisionError(\"The sum of denominator1 and denominator2 is zero!\")\n", " \n", " return numerator / (denominator1 + denominator2)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false }, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "The sum of denominator1 and denominator2 is zero!", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\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[0mdivide_sum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m1\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;36mdivide_sum\u001b[0;34m(numerator, denominator1, denominator2)\u001b[0m\n\u001b[1;32m 21\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdenominator1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mdenominator2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 23\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mZeroDivisionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"The sum of denominator1 and denominator2 is zero!\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 24\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mnumerator\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdenominator1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mdenominator2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mZeroDivisionError\u001b[0m: The sum of denominator1 and denominator2 is zero!" ] } ], "source": [ "divide_sum(1, 1, -1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are [a large number of standard exceptions](https://docs.python.org/2/library/exceptions.html) in Python, and most of the time you should use one of those, combined with a meaningful error message. One is particularly useful: `NotImplementedError`.\n", "\n", "This exception is used when the behaviour the code is about to attempt makes no sense, is not defined, or similar. For example, consider computing the roots of the quadratic equation, but restricting to only real solutions. Using the standard formula\n", "\n", "$$ x_{\\pm} = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a} $$\n", "\n", "we know that this only makes sense if $b^2 \\ge 4ac$. We put this in code as:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", " \n", "def real_quadratic_roots(a, b, c):\n", " \"\"\"\n", " Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.\n", " \n", " Parameters\n", " ----------\n", " \n", " a : float\n", " Coefficient of x^2\n", " b : float\n", " Coefficient of x^1\n", " c : float\n", " Coefficient of x^0\n", " \n", " Returns\n", " -------\n", " \n", " roots : tuple\n", " The roots\n", " \n", " Raises\n", " ------\n", " \n", " NotImplementedError\n", " If the roots are not real.\n", " \"\"\"\n", " \n", " discriminant = b**2 - 4.0*a*c\n", " if discriminant < 0.0:\n", " raise NotImplementedError(\"The discriminant is {} < 0. \"\n", " \"No real roots exist.\".format(discriminant))\n", " \n", " x_plus = (-b + sqrt(discriminant)) / (2.0*a)\n", " x_minus = (-b - sqrt(discriminant)) / (2.0*a)\n", " \n", " return x_plus, x_minus" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(-2.0, -3.0)\n" ] } ], "source": [ "print(real_quadratic_roots(1.0, 5.0, 6.0))" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "ename": "NotImplementedError", "evalue": "The discriminant is -19.0 < 0. No real roots exist.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNotImplementedError\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[0mreal_quadratic_roots\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;36m5.0\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;36mreal_quadratic_roots\u001b[0;34m(a, b, c)\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mdiscriminant\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m0.0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 32\u001b[0m raise NotImplementedError(\"The discriminant is {} < 0. \"\n\u001b[0;32m---> 33\u001b[0;31m \"No real roots exist.\".format(discriminant))\n\u001b[0m\u001b[1;32m 34\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[0mx_plus\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0mb\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msqrt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdiscriminant\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m2.0\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNotImplementedError\u001b[0m: The discriminant is -19.0 < 0. No real roots exist." ] } ], "source": [ "real_quadratic_roots(1.0, 1.0, 5.0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Testing\n", "\n", "How do we know if our code is working correctly? It is not when the code runs and returns some value: as seen above, there may be times where it makes sense to stop the code even when it is correct, as it is being used incorrectly. We need to test the code to check that it works.\n", "\n", "*Unit testing* is the idea of writing many small tests that check if simple cases are behaving correctly. Rather than trying to *prove* that the code is correct in all cases (which could be very hard), we check that it is correct in a number of tightly controlled cases (which should be more straightforward). If we later find a problem with the code, we add a test to cover that case.\n", "\n", "Consider a function solving for the real roots of the quadratic equation again. This time, if there are no real roots we shall return `None` (to say there are no roots) instead of raising an exception." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", " \n", "def real_quadratic_roots(a, b, c):\n", " \"\"\"\n", " Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.\n", " \n", " Parameters\n", " ----------\n", " \n", " a : float\n", " Coefficient of x^2\n", " b : float\n", " Coefficient of x^1\n", " c : float\n", " Coefficient of x^0\n", " \n", " Returns\n", " -------\n", " \n", " roots : tuple or None\n", " The roots\n", " \"\"\"\n", " \n", " discriminant = b**2 - 4.0*a*c\n", " if discriminant < 0.0:\n", " return None\n", " \n", " x_plus = (-b + sqrt(discriminant)) / (2.0*a)\n", " x_minus = (-b + sqrt(discriminant)) / (2.0*a)\n", " \n", " return x_plus, x_minus" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First we check what happens if there are imaginary roots, using $x^2 + 1 = 0$:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "None\n" ] } ], "source": [ "print(real_quadratic_roots(1, 0, 1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we wanted, it has returned `None`. We also check what happens if the roots are zero, using $x^2 = 0$:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(0.0, 0.0)\n" ] } ], "source": [ "print(real_quadratic_roots(1, 0, 0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We get the expected behaviour. We also check what happens if the roots are real, using $x^2 - 1 = 0$ which has roots $\\pm 1$:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(1.0, 1.0)\n" ] } ], "source": [ "print(real_quadratic_roots(1, 0, -1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Something has gone wrong. Looking at the code, we see that the `x_minus` line has been copied and pasted from the `x_plus` line, without changing the sign correctly. So we fix that error:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", " \n", "def real_quadratic_roots(a, b, c):\n", " \"\"\"\n", " Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.\n", " \n", " Parameters\n", " ----------\n", " \n", " a : float\n", " Coefficient of x^2\n", " b : float\n", " Coefficient of x^1\n", " c : float\n", " Coefficient of x^0\n", " \n", " Returns\n", " -------\n", " \n", " roots : tuple or None\n", " The roots\n", " \"\"\"\n", " \n", " discriminant = b**2 - 4.0*a*c\n", " if discriminant < 0.0:\n", " return None\n", " \n", " x_plus = (-b + sqrt(discriminant)) / (2.0*a)\n", " x_minus = (-b - sqrt(discriminant)) / (2.0*a)\n", " \n", " return x_plus, x_minus" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have changed the code, so now have to re-run *all* our tests, in case our change broke something else:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "None\n", "(0.0, 0.0)\n", "(1.0, -1.0)\n" ] } ], "source": [ "print(real_quadratic_roots(1, 0, 1))\n", "print(real_quadratic_roots(1, 0, 0))\n", "print(real_quadratic_roots(1, 0, -1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a final test, we check what happens if the equation degenerates to a linear equation where $a=0$, using $x + 1 = 0$ with solution $-1$:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false }, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "float division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\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[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreal_quadratic_roots\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;36m1\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;36mreal_quadratic_roots\u001b[0;34m(a, b, c)\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 27\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 28\u001b[0;31m \u001b[0mx_plus\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0mb\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msqrt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdiscriminant\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m2.0\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 29\u001b[0m \u001b[0mx_minus\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0mb\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0msqrt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdiscriminant\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m2.0\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mZeroDivisionError\u001b[0m: float division by zero" ] } ], "source": [ "print(real_quadratic_roots(0, 1, 1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this case we get an exception, which we don't want. We fix this problem:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", " \n", "def real_quadratic_roots(a, b, c):\n", " \"\"\"\n", " Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.\n", " \n", " Parameters\n", " ----------\n", " \n", " a : float\n", " Coefficient of x^2\n", " b : float\n", " Coefficient of x^1\n", " c : float\n", " Coefficient of x^0\n", " \n", " Returns\n", " -------\n", " \n", " roots : tuple or float or None\n", " The root(s) (two if a genuine quadratic, one if linear, None otherwise)\n", " \n", " Raises\n", " ------\n", " \n", " NotImplementedError\n", " If the equation has trivial a and b coefficients, so isn't solvable.\n", " \"\"\"\n", " \n", " discriminant = b**2 - 4.0*a*c\n", " if discriminant < 0.0:\n", " return None\n", " \n", " if a == 0:\n", " if b == 0:\n", " raise NotImplementedError(\"Cannot solve quadratic with both a\"\n", " \" and b coefficients equal to 0.\")\n", " else:\n", " return -c / b\n", " \n", " x_plus = (-b + sqrt(discriminant)) / (2.0*a)\n", " x_minus = (-b - sqrt(discriminant)) / (2.0*a)\n", " \n", " return x_plus, x_minus" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we now must re-run all our tests again, as the code has changed once more:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "None\n", "(0.0, 0.0)\n", "(1.0, -1.0)\n", "-1.0\n" ] } ], "source": [ "print(real_quadratic_roots(1, 0, 1))\n", "print(real_quadratic_roots(1, 0, 0))\n", "print(real_quadratic_roots(1, 0, -1))\n", "print(real_quadratic_roots(0, 1, 1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Formalizing tests\n", "\n", "This small set of tests covers most of the cases we are concerned with. However, by this point it's getting hard to remember\n", "\n", "1. what each line is actually testing, and\n", "2. what the correct value is meant to be.\n", "\n", "To formalize this, we write each test as a small function that contains this information for us. Let's start with the $x^2 - 1 = 0$ case where the roots are $\\pm 1$:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from numpy.testing import assert_equal, assert_allclose\n", "\n", "def test_real_distinct():\n", " \"\"\"\n", " Test that the roots of x^2 - 1 = 0 are \\pm 1.\n", " \"\"\"\n", " \n", " roots = (1.0, -1.0)\n", " assert_equal(real_quadratic_roots(1, 0, -1), roots,\n", " err_msg=\"Testing x^2-1=0; roots should be 1 and -1.\")" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false }, "outputs": [], "source": [ "test_real_distinct()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What this function does is checks that the results of the function call match the expected value, here stored in `roots`. If it didn't match the expected value, it would raise an exception:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": false }, "outputs": [ { "ename": "AssertionError", "evalue": "\nItems are not equal:\nitem=1\nTesting x^2-1=0; roots should be 1 and 1. So this test should fail\n ACTUAL: -1.0\n DESIRED: 1.0", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\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 9\u001b[0m \" So this test should fail\")\n\u001b[1;32m 10\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 11\u001b[0;31m \u001b[0mtest_should_fail\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_should_fail\u001b[0;34m()\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0mroots\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[0m\n\u001b[1;32m 7\u001b[0m assert_equal(real_quadratic_roots(1, 0, -1), roots,\n\u001b[0;32m----> 8\u001b[0;31m \u001b[0merr_msg\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"Testing x^2-1=0; roots should be 1 and 1.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 9\u001b[0m \" So this test should fail\")\n\u001b[1;32m 10\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Users/ih3/anaconda/lib/python3.4/site-packages/numpy/testing/utils.py\u001b[0m in \u001b[0;36massert_equal\u001b[0;34m(actual, desired, err_msg, verbose)\u001b[0m\n\u001b[1;32m 288\u001b[0m \u001b[0massert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactual\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0merr_msg\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mverbose\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 289\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 290\u001b[0;31m \u001b[0massert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactual\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdesired\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'item=%r\\n%s'\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0merr_msg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mverbose\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 291\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mndarray\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0misscalar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msignbit\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Users/ih3/anaconda/lib/python3.4/site-packages/numpy/testing/utils.py\u001b[0m in \u001b[0;36massert_equal\u001b[0;34m(actual, desired, err_msg, verbose)\u001b[0m\n\u001b[1;32m 352\u001b[0m \u001b[0;31m# Explicitly use __eq__ for comparison, ticket #2552\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 353\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mactual\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 354\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 355\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 356\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mprint_assert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtest_string\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactual\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: \nItems are not equal:\nitem=1\nTesting x^2-1=0; roots should be 1 and 1. So this test should fail\n ACTUAL: -1.0\n DESIRED: 1.0" ] } ], "source": [ "def test_should_fail():\n", " \"\"\"\n", " Comparing the roots of x^2 - 1 = 0 to (1, 1), which should fail.\n", " \"\"\"\n", " \n", " roots = (1.0, 1.0)\n", " assert_equal(real_quadratic_roots(1, 0, -1), roots,\n", " err_msg=\"Testing x^2-1=0; roots should be 1 and 1.\"\n", " \" So this test should fail\")\n", "\n", "test_should_fail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Testing that one floating point number equals another can be dangerous. Consider $x^2 - 2 x + (1 - 10^{-10}) = 0$ with roots $1.1 \\pm 10^{-5} )$:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": false }, "outputs": [ { "ename": "AssertionError", "evalue": "\nItems are not equal:\nitem=0\nTesting x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\n ACTUAL: 1.0000100000004137\n DESIRED: 1.00001", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\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 10\u001b[0m err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n\u001b[1;32m 11\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 12\u001b[0;31m \u001b[0mtest_real_distinct_irrational\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_real_distinct_irrational\u001b[0;34m()\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mroots\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1e-5\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1e-5\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m assert_equal(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,\n\u001b[0;32m---> 10\u001b[0;31m err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mtest_real_distinct_irrational\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Users/ih3/anaconda/lib/python3.4/site-packages/numpy/testing/utils.py\u001b[0m in \u001b[0;36massert_equal\u001b[0;34m(actual, desired, err_msg, verbose)\u001b[0m\n\u001b[1;32m 288\u001b[0m \u001b[0massert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactual\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0merr_msg\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mverbose\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 289\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mk\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 290\u001b[0;31m \u001b[0massert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactual\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdesired\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'item=%r\\n%s'\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0merr_msg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mverbose\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 291\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mndarray\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0misscalar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msignbit\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Users/ih3/anaconda/lib/python3.4/site-packages/numpy/testing/utils.py\u001b[0m in \u001b[0;36massert_equal\u001b[0;34m(actual, desired, err_msg, verbose)\u001b[0m\n\u001b[1;32m 352\u001b[0m \u001b[0;31m# Explicitly use __eq__ for comparison, ticket #2552\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 353\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mdesired\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mactual\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 354\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmsg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 355\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 356\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mprint_assert_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtest_string\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mactual\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdesired\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: \nItems are not equal:\nitem=0\nTesting x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\n ACTUAL: 1.0000100000004137\n DESIRED: 1.00001" ] } ], "source": [ "from math import sqrt\n", "\n", "def test_real_distinct_irrational():\n", " \"\"\"\n", " Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \\pm 1e-5.\n", " \"\"\"\n", " \n", " roots = (1 + 1e-5, 1 - 1e-5)\n", " assert_equal(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,\n", " err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n", " \n", "test_real_distinct_irrational()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that the solutions match to the first 14 or so digits, but this isn't enough for them to be *exactly* the same. In this case, and in most cases using floating point numbers, we want the result to be \"close enough\": to match the expected precision. There is an assertion for this as well:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from math import sqrt\n", "\n", "def test_real_distinct_irrational():\n", " \"\"\"\n", " Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \\pm 1e-5.\n", " \"\"\"\n", " \n", " roots = (1 + 1e-5, 1 - 1e-5)\n", " assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,\n", " err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n", " \n", "test_real_distinct_irrational()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `assert_allclose` statement takes options controlling the precision of our test.\n", "\n", "We can now write out all our tests:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from math import sqrt\n", "from numpy.testing import assert_equal, assert_allclose\n", "\n", "def test_no_roots():\n", " \"\"\"\n", " Test that the roots of x^2 + 1 = 0 are not real.\n", " \"\"\"\n", " \n", " roots = None\n", " assert_equal(real_quadratic_roots(1, 0, 1), roots,\n", " err_msg=\"Testing x^2+1=0; no real roots.\")\n", "\n", "def test_zero_roots():\n", " \"\"\"\n", " Test that the roots of x^2 = 0 are both zero.\n", " \"\"\"\n", " \n", " roots = (0, 0)\n", " assert_equal(real_quadratic_roots(1, 0, 0), roots,\n", " err_msg=\"Testing x^2=0; should both be zero.\")\n", "\n", "def test_real_distinct():\n", " \"\"\"\n", " Test that the roots of x^2 - 1 = 0 are \\pm 1.\n", " \"\"\"\n", " \n", " roots = (1.0, -1.0)\n", " assert_equal(real_quadratic_roots(1, 0, -1), roots,\n", " err_msg=\"Testing x^2-1=0; roots should be 1 and -1.\")\n", " \n", "def test_real_distinct_irrational():\n", " \"\"\"\n", " Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \\pm 1e-5.\n", " \"\"\"\n", " \n", " roots = (1 + 1e-5, 1 - 1e-5)\n", " assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,\n", " err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n", " \n", "def test_real_linear_degeneracy():\n", " \"\"\"\n", " Test that the root of x + 1 = 0 is -1.\n", " \"\"\"\n", " \n", " root = -1.0\n", " assert_equal(real_quadratic_roots(0, 1, 1), root,\n", " err_msg=\"Testing x+1=0; root should be -1.\")" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "collapsed": false }, "outputs": [], "source": [ "test_no_roots()\n", "test_zero_roots()\n", "test_real_distinct()\n", "test_real_distinct_irrational()\n", "test_real_linear_degeneracy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Nose\n", "\n", "We now have a set of tests - a *testsuite*, as it is sometimes called - encoded in functions, with meaningful names, which give useful error messages if the test fails. Every time the code is changed, we want to re-run all the tests to ensure that our change has not broken the code. This can be tedious. A better way would be to run a single command that runs all tests. `nosetests` is that command.\n", "\n", "The easiest way to use it is to put all tests in the same file as the function being tested. So, create a file `quadratic.py` containing\n", "\n", "```python\n", "from math import sqrt\n", "from numpy.testing import assert_equal, assert_allclose\n", " \n", "def real_quadratic_roots(a, b, c):\n", " \"\"\"\n", " Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.\n", " \n", " Parameters\n", " ----------\n", " \n", " a : float\n", " Coefficient of x^2\n", " b : float\n", " Coefficient of x^1\n", " c : float\n", " Coefficient of x^0\n", " \n", " Returns\n", " -------\n", " \n", " roots : tuple or float or None\n", " The root(s) (two if a genuine quadratic, one if linear, None otherwise)\n", " \n", " Raises\n", " ------\n", " \n", " NotImplementedError\n", " If the equation has trivial a and b coefficients, so isn't solvable.\n", " \"\"\"\n", " \n", " discriminant = b**2 - 4.0*a*c\n", " if discriminant < 0.0:\n", " return None\n", " \n", " if a == 0:\n", " if b == 0:\n", " raise NotImplementedError(\"Cannot solve quadratic with both a\"\n", " \" and b coefficients equal to 0.\")\n", " else:\n", " return -c / b\n", " \n", " x_plus = (-b + sqrt(discriminant)) / (2.0*a)\n", " x_minus = (-b - sqrt(discriminant)) / (2.0*a)\n", " \n", " return x_plus, x_minus\n", "\n", "def test_no_roots():\n", " \"\"\"\n", " Test that the roots of x^2 + 1 = 0 are not real.\n", " \"\"\"\n", " \n", " roots = None\n", " assert_equal(real_quadratic_roots(1, 0, 1), roots,\n", " err_msg=\"Testing x^2+1=0; no real roots.\")\n", "\n", "def test_zero_roots():\n", " \"\"\"\n", " Test that the roots of x^2 = 0 are both zero.\n", " \"\"\"\n", " \n", " roots = (0, 0)\n", " assert_equal(real_quadratic_roots(1, 0, 0), roots,\n", " err_msg=\"Testing x^2=0; should both be zero.\")\n", "\n", "def test_real_distinct():\n", " \"\"\"\n", " Test that the roots of x^2 - 1 = 0 are \\pm 1.\n", " \"\"\"\n", " \n", " roots = (1.0, -1.0)\n", " assert_equal(real_quadratic_roots(1, 0, -1), roots,\n", " err_msg=\"Testing x^2-1=0; roots should be 1 and -1.\")\n", " \n", "def test_real_distinct_irrational():\n", " \"\"\"\n", " Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \\pm 1e-5.\n", " \"\"\"\n", " \n", " roots = (1 + 1e-5, 1 - 1e-5)\n", " assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,\n", " err_msg=\"Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.\")\n", " \n", "def test_real_linear_degeneracy():\n", " \"\"\"\n", " Test that the root of x + 1 = 0 is -1.\n", " \"\"\"\n", " \n", " root = -1.0\n", " assert_equal(real_quadratic_roots(0, 1, 1), root,\n", " err_msg=\"Testing x+1=0; root should be -1.\")\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Then, in a terminal or command window, switch to the directory containing this file. Then run\n", "\n", "```\n", "nosetests quadratic.py\n", "```\n", "\n", "You should see output similar to\n", "\n", "```\n", "nosetests quadratic.py \n", ".....\n", "----------------------------------------------------------------------\n", "Ran 5 tests in 0.006s\n", "\n", "OK\n", "```\n", "\n", "Each dot corresponds to a test. If a test fails, `nose` will report the error and move on to the next test. `nose` automatically runs every function that starts with `test`, or every file in a module starting with `test`, or more. [The documentation](https://nose.readthedocs.org/en/latest/testing.html) gives more details about using `nose` in more complex cases.\n", "\n", "To summarize: when trying to get code working, tests are essential. Tests should be simple and cover as many of the easy cases and as much of the code as possible. By writing tests as functions that raise exceptions, and using a testing framework such as `nose`, all tests can be run rapidly, saving time.\n", "\n", "## Test Driven Development\n", "\n", "There are many ways of writing code to solve problems. Most involve planning in advance how the code should be written. An alternative is to say in advance what tests the code should pass. This *Test Driven Development* (TDD) has advantages (the code always has a detailed set of tests, features in the code are always relevant to some test, it's easy to start writing code) and some disadvantages (it can be overkill for small projects, it can lead down blind alleys). A [detailed discussion is given by Beck's book](http://www.amazon.co.uk/Driven-Development-Addison-Wesley-Signature-Series/dp/0321146530), and a [more recent discussion in this series of conversations](http://martinfowler.com/articles/is-tdd-dead/).\n", "\n", "Even if TDD does not work for you, testing itself is extremely important." ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python [default]", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" }, "nbconvert": { "title": "Exceptions and testing" } }, "nbformat": 4, "nbformat_minor": 0 }