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.
These special cases can be dealt with using exceptions.
Let's define a function that divides two numbers.
In [1]:
from __future__ import division
In [2]:
def divide(numerator, denominator):
"""
Divide two numbers.
Parameters
----------
numerator: float
numerator
denominator: float
denominator
Returns
-------
fraction: float
numerator / denominator
"""
return numerator / denominator
In [3]:
print(divide(4.0, 5.0))
But what happens if we try something really stupid?
In [4]:
print(divide(4.0, 0.0))
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:
In [5]:
denominators = [1.0, 0.0, 3.0, 5.0]
for denominator in denominators:
print(divide(4.0, denominator))
There are three sensible results, but we only get the first.
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:
In [6]:
try:
print(divide(4.0, 0.0))
except ZeroDivisionError:
print("Dividing by zero is a silly thing to do!")
In [7]:
denominators = [1.0, 0.0, 3.0, 5.0]
for denominator in denominators:
try:
print(divide(4.0, denominator))
except ZeroDivisionError:
print("Dividing by zero is a silly thing to do!")
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.
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.
To show how this works with different errors, consider a different silly error:
In [8]:
try:
print(divide(4.0, "zero"))
except ZeroDivisionError:
print("Dividing by zero is a silly thing to do!")
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:
In [9]:
try:
print(divide(4.0, "zero"))
except ZeroDivisionError:
print("Dividing by zero is a silly thing to do!")
except TypeError:
print("Dividing by a string is a silly thing to do!")
We could catch any error:
In [10]:
try:
print(divide(4.0, "zero"))
except:
print("Some error occured")
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:
In [11]:
try:
print(divide(4.0, "zero"))
except (ZeroDivisionError, TypeError) as exception:
print("Some error occured: {}".format(exception))
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.
Normally best practise is to be as specific as possible on the error you are trying to catch.
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.
One way of doing this would be the following:
In [12]:
denominators = [1.0, 0.0, 3.0, "zero", 5.0]
results = []
divisors = []
for denominator in denominators:
try:
result = divide(4.0, denominator)
except (ZeroDivisionError, TypeError) as exception:
print("Error of type {} for denominator {}".format(exception, denominator))
else:
results.append(result)
divisors.append(denominator)
print(results)
print(divisors)
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.
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:
In [13]:
def divide_sum(numerator, denominator1, denominator2):
"""
Divide a number by a sum.
Parameters
----------
numerator: float
numerator
denominator1: float
Part of the denominator
denominator2: float
Part of the denominator
Returns
-------
fraction: float
numerator / (denominator1 + denominator2)
"""
return numerator / (denominator1 + denominator2)
In [14]:
divide_sum(1, 1, -1)
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:
In [15]:
def divide_sum(numerator, denominator1, denominator2):
"""
Divide a number by a sum.
Parameters
----------
numerator: float
numerator
denominator1: float
Part of the denominator
denominator2: float
Part of the denominator
Returns
-------
fraction: float
numerator / (denominator1 + denominator2)
"""
if (denominator1 + denominator2) == 0:
raise ZeroDivisionError("The sum of denominator1 and denominator2 is zero!")
return numerator / (denominator1 + denominator2)
In [16]:
divide_sum(1, 1, -1)
There are a large number of standard exceptions in Python, and most of the time you should use one of those, combined with a meaningful error message. One is particularly useful: NotImplementedError
.
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
$$ x_{\pm} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$we know that this only makes sense if $b^2 \ge 4ac$. We put this in code as:
In [17]:
from math import sqrt
def real_quadratic_roots(a, b, c):
"""
Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
Parameters
----------
a : float
Coefficient of x^2
b : float
Coefficient of x^1
c : float
Coefficient of x^0
Returns
-------
roots : tuple
The roots
Raises
------
NotImplementedError
If the roots are not real.
"""
discriminant = b**2 - 4.0*a*c
if discriminant < 0.0:
raise NotImplementedError("The discriminant is {} < 0. "
"No real roots exist.".format(discriminant))
x_plus = (-b + sqrt(discriminant)) / (2.0*a)
x_minus = (-b - sqrt(discriminant)) / (2.0*a)
return x_plus, x_minus
In [18]:
print(real_quadratic_roots(1.0, 5.0, 6.0))
In [19]:
real_quadratic_roots(1.0, 1.0, 5.0)
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.
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.
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.
In [20]:
from math import sqrt
def real_quadratic_roots(a, b, c):
"""
Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
Parameters
----------
a : float
Coefficient of x^2
b : float
Coefficient of x^1
c : float
Coefficient of x^0
Returns
-------
roots : tuple or None
The roots
"""
discriminant = b**2 - 4.0*a*c
if discriminant < 0.0:
return None
x_plus = (-b + sqrt(discriminant)) / (2.0*a)
x_minus = (-b + sqrt(discriminant)) / (2.0*a)
return x_plus, x_minus
First we check what happens if there are imaginary roots, using $x^2 + 1 = 0$:
In [21]:
print(real_quadratic_roots(1, 0, 1))
As we wanted, it has returned None
. We also check what happens if the roots are zero, using $x^2 = 0$:
In [22]:
print(real_quadratic_roots(1, 0, 0))
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$:
In [23]:
print(real_quadratic_roots(1, 0, -1))
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:
In [24]:
from math import sqrt
def real_quadratic_roots(a, b, c):
"""
Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
Parameters
----------
a : float
Coefficient of x^2
b : float
Coefficient of x^1
c : float
Coefficient of x^0
Returns
-------
roots : tuple or None
The roots
"""
discriminant = b**2 - 4.0*a*c
if discriminant < 0.0:
return None
x_plus = (-b + sqrt(discriminant)) / (2.0*a)
x_minus = (-b - sqrt(discriminant)) / (2.0*a)
return x_plus, x_minus
We have changed the code, so now have to re-run all our tests, in case our change broke something else:
In [25]:
print(real_quadratic_roots(1, 0, 1))
print(real_quadratic_roots(1, 0, 0))
print(real_quadratic_roots(1, 0, -1))
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$:
In [26]:
print(real_quadratic_roots(0, 1, 1))
In this case we get an exception, which we don't want. We fix this problem:
In [27]:
from math import sqrt
def real_quadratic_roots(a, b, c):
"""
Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
Parameters
----------
a : float
Coefficient of x^2
b : float
Coefficient of x^1
c : float
Coefficient of x^0
Returns
-------
roots : tuple or float or None
The root(s) (two if a genuine quadratic, one if linear, None otherwise)
Raises
------
NotImplementedError
If the equation has trivial a and b coefficients, so isn't solvable.
"""
discriminant = b**2 - 4.0*a*c
if discriminant < 0.0:
return None
if a == 0:
if b == 0:
raise NotImplementedError("Cannot solve quadratic with both a"
" and b coefficients equal to 0.")
else:
return -c / b
x_plus = (-b + sqrt(discriminant)) / (2.0*a)
x_minus = (-b - sqrt(discriminant)) / (2.0*a)
return x_plus, x_minus
And we now must re-run all our tests again, as the code has changed once more:
In [28]:
print(real_quadratic_roots(1, 0, 1))
print(real_quadratic_roots(1, 0, 0))
print(real_quadratic_roots(1, 0, -1))
print(real_quadratic_roots(0, 1, 1))
This small set of tests covers most of the cases we are concerned with. However, by this point it's getting hard to remember
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$:
In [29]:
from numpy.testing import assert_equal, assert_allclose
def test_real_distinct():
"""
Test that the roots of x^2 - 1 = 0 are \pm 1.
"""
roots = (1.0, -1.0)
assert_equal(real_quadratic_roots(1, 0, -1), roots,
err_msg="Testing x^2-1=0; roots should be 1 and -1.")
In [30]:
test_real_distinct()
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:
In [31]:
def test_should_fail():
"""
Comparing the roots of x^2 - 1 = 0 to (1, 1), which should fail.
"""
roots = (1.0, 1.0)
assert_equal(real_quadratic_roots(1, 0, -1), roots,
err_msg="Testing x^2-1=0; roots should be 1 and 1."
" So this test should fail")
test_should_fail()
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} )$:
In [32]:
from math import sqrt
def test_real_distinct_irrational():
"""
Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
"""
roots = (1 + 1e-5, 1 - 1e-5)
assert_equal(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
test_real_distinct_irrational()
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:
In [33]:
from math import sqrt
def test_real_distinct_irrational():
"""
Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
"""
roots = (1 + 1e-5, 1 - 1e-5)
assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
test_real_distinct_irrational()
The assert_allclose
statement takes options controlling the precision of our test.
We can now write out all our tests:
In [34]:
from math import sqrt
from numpy.testing import assert_equal, assert_allclose
def test_no_roots():
"""
Test that the roots of x^2 + 1 = 0 are not real.
"""
roots = None
assert_equal(real_quadratic_roots(1, 0, 1), roots,
err_msg="Testing x^2+1=0; no real roots.")
def test_zero_roots():
"""
Test that the roots of x^2 = 0 are both zero.
"""
roots = (0, 0)
assert_equal(real_quadratic_roots(1, 0, 0), roots,
err_msg="Testing x^2=0; should both be zero.")
def test_real_distinct():
"""
Test that the roots of x^2 - 1 = 0 are \pm 1.
"""
roots = (1.0, -1.0)
assert_equal(real_quadratic_roots(1, 0, -1), roots,
err_msg="Testing x^2-1=0; roots should be 1 and -1.")
def test_real_distinct_irrational():
"""
Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
"""
roots = (1 + 1e-5, 1 - 1e-5)
assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
def test_real_linear_degeneracy():
"""
Test that the root of x + 1 = 0 is -1.
"""
root = -1.0
assert_equal(real_quadratic_roots(0, 1, 1), root,
err_msg="Testing x+1=0; root should be -1.")
In [35]:
test_no_roots()
test_zero_roots()
test_real_distinct()
test_real_distinct_irrational()
test_real_linear_degeneracy()
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.
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
from math import sqrt
from numpy.testing import assert_equal, assert_allclose
def real_quadratic_roots(a, b, c):
"""
Find the real roots of the quadratic equation a x^2 + b x + c = 0, if they exist.
Parameters
----------
a : float
Coefficient of x^2
b : float
Coefficient of x^1
c : float
Coefficient of x^0
Returns
-------
roots : tuple or float or None
The root(s) (two if a genuine quadratic, one if linear, None otherwise)
Raises
------
NotImplementedError
If the equation has trivial a and b coefficients, so isn't solvable.
"""
discriminant = b**2 - 4.0*a*c
if discriminant < 0.0:
return None
if a == 0:
if b == 0:
raise NotImplementedError("Cannot solve quadratic with both a"
" and b coefficients equal to 0.")
else:
return -c / b
x_plus = (-b + sqrt(discriminant)) / (2.0*a)
x_minus = (-b - sqrt(discriminant)) / (2.0*a)
return x_plus, x_minus
def test_no_roots():
"""
Test that the roots of x^2 + 1 = 0 are not real.
"""
roots = None
assert_equal(real_quadratic_roots(1, 0, 1), roots,
err_msg="Testing x^2+1=0; no real roots.")
def test_zero_roots():
"""
Test that the roots of x^2 = 0 are both zero.
"""
roots = (0, 0)
assert_equal(real_quadratic_roots(1, 0, 0), roots,
err_msg="Testing x^2=0; should both be zero.")
def test_real_distinct():
"""
Test that the roots of x^2 - 1 = 0 are \pm 1.
"""
roots = (1.0, -1.0)
assert_equal(real_quadratic_roots(1, 0, -1), roots,
err_msg="Testing x^2-1=0; roots should be 1 and -1.")
def test_real_distinct_irrational():
"""
Test that the roots of x^2 - 2 x + (1 - 10**(-10)) = 0 are 1 \pm 1e-5.
"""
roots = (1 + 1e-5, 1 - 1e-5)
assert_allclose(real_quadratic_roots(1, -2.0, 1.0 - 1e-10), roots,
err_msg="Testing x^2-2x+(1-1e-10)=0; roots should be 1 +- 1e-5.")
def test_real_linear_degeneracy():
"""
Test that the root of x + 1 = 0 is -1.
"""
root = -1.0
assert_equal(real_quadratic_roots(0, 1, 1), root,
err_msg="Testing x+1=0; root should be -1.")
Then, in a terminal or command window, switch to the directory containing this file. Then run
nosetests quadratic.py
You should see output similar to
nosetests quadratic.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.006s
OK
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 gives more details about using nose
in more complex cases.
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.
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, and a more recent discussion in this series of conversations.
Even if TDD does not work for you, testing itself is extremely important.