In [ ]:
import numpy as np
Today we'll cover perhaps one of the most important aspects of using Python: dealing with errors and bugs in code.
Types of bugs/errors in code, from the easiest to the most difficult to diagnose:
In [ ]:
X = [1, 2, 3)
In [ ]:
y = 4x + 3
Note that if your code contains even a single syntax error, none of it will run:
In [ ]:
a = 4
something ==== is wrong
In [ ]:
print(a)
Even though the syntax error appears below the (valid) variable definition, the valid code is not executed.
In [ ]:
print(Q)
In [ ]:
x = 1 + 'abc'
In [ ]:
X = 1 / 0
In [ ]:
import numpy as np
np.add(1, 2, 3, 4)
In [ ]:
x = [1, 2, 3]
print(x[100])
Unlike Syntax errors, RunTime errors occur during code execution, which means that valid code occuring before the runtime error will execute:
In [ ]:
spam = "my all-time favorite"
eggs = 1 / 0
In [ ]:
print(spam)
Semantic errors are perhaps the most insidious errors, and are by far the ones that will take most of your time. Semantic errors occur when the code is syntactically correct, but produces the wrong result.
By way of example, imagine you want to write a simple script to approximate the value of $\pi$ according to the following formula:
$$ \pi = \sqrt{12} \sum_{k = 0}^{\infty} \frac{(-3)^{-k}}{2k + 1} $$You might write a function something like this, using numpy's vectorized syntax:
In [ ]:
from math import sqrt
def approx_pi(nterms=100):
kvals = np.arange(nterms)
return sqrt(12) * np.sum([-3.0 ** -k / (2 * k + 1) for k in kvals])
Looks OK, yes? Let's try it out:
In [ ]:
approx_pi(100)
Huh. That doesn't look like $\pi$. Maybe we need more terms?
In [ ]:
approx_pi(1000)
Nope... it looks like the algorithm simply gives the wrong result. This is a classic example of a semantic error.
Question: can you spot the problem?
In [ ]:
try:
print("this block gets executed first")
except:
print("this block gets executed if there's an error")
In [ ]:
try:
print("this block gets executed first")
x = 1 / 0 # ZeroDivisionError
print("we never get here")
except:
print("this block gets executed if there's an error")
Notice that the first block executes up until the point of the Runtime error.
Once the error is hit, the except
block is executed.
One important note: the above clause catches any and all exceptions. It is not generally a good idea to catch-all. Better is to name the precise exception you expect:
In [ ]:
def safe_divide(a, b):
try:
return a / b
except:
print("oops, dividing by zero. Returning None.")
return None
print(safe_divide(15, 3))
print(safe_divide(1, 0))
But there's a problem here: this is a catch-all exception, and will sometimes give us misleading information. For example:
In [ ]:
safe_divide(15, 'three')
Our program tells us we're dividing by zero, but we aren't! This is one reason you should almost never use a catch-all try..except
statement, but instead specify the errors you're trying to catch:
In [ ]:
def better_safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("oops, dividing by zero. Returning None.")
return None
better_safe_divide(15, 0)
In [ ]:
better_safe_divide(15, 'three')
This also allows you to specify different behaviors for different exceptions:
In [ ]:
def even_better_safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("oops, dividing by zero. Returning None.")
return None
except TypeError:
print("incompatible types. Returning None")
return None
In [ ]:
even_better_safe_divide(15, 3)
In [ ]:
even_better_safe_divide(15, 0)
In [ ]:
even_better_safe_divide(15, 'three')
Remember this lesson, and always specify your except statements! I once spent an entire day tracing down a bug in my code which amounted to this.
In [ ]:
import os # the "os" module has useful operating system stuff
def read_file(filename):
if not os.path.exists(filename):
raise ValueError("'{0}' does not exist".format(filename))
f = open(filename)
result = f.read()
f.close()
return result
We'll use IPython's %%file
magic to quickly create a text file
In [ ]:
%%file tmp.txt
this is the contents of the file
In [ ]:
read_file('tmp.txt')
In [ ]:
read_file('file.which.does.not.exist')
It is sometimes useful to define your own custom exceptions, which you can do easily via class inheritance:
In [ ]:
class NonExistentFile(RuntimeError):
# you can customize exception behavior by defining class methods.
# we won't discuss that here.
pass
def read_file(filename):
if not os.path.exists(filename):
raise NonExistentFile(filename)
f = open(filename)
result = f.read()
f.close()
return result
In [ ]:
read_file('tmp.txt')
In [ ]:
read_file('file.which.does.not.exist')
Get used to throwing appropriate — and meaningful — exceptions in your code! It makes reading and debugging your code much, much easier.
In [ ]:
try:
print("doing something")
except:
print("this only happens if it fails")
else:
print("this only happens if it succeeds")
In [ ]:
try:
print("doing something")
raise ValueError()
except:
print("this only happens if it fails")
else:
print("this only happens if it succeeds")
Why would you ever want to do this?
Mainly, it prevents the code within the else
block from being caught by the try
block.
Accidentally catching an exception you don't mean to catch can lead to confusing results.
The last statement you might use is the finally
statement, which looks like this:
In [ ]:
try:
print("do something")
except:
print("this only happens if it fails")
else:
print("this only happens if it succeeds")
finally:
print("this happens no matter what.")
In [ ]:
try:
print("do something")
raise ValueError()
except:
print("this only happens if it fails")
else:
print("this only happens if it succeeds")
finally:
print("this happens no matter what.")
finally
is generally used for some sort of cleanup (closing a file, etc.) It might seem a bit redundant, though. Why not write the following?
In [ ]:
try:
print("do something")
except:
print("this only happens if it fails")
else:
print("this only happens if it succeeds")
print("this happens no matter what.")
The main difference is when the clause is used within a function:
In [ ]:
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
return None
else:
print("result is", result)
return result
finally:
print("some sort of cleanup")
In [ ]:
divide(15, 3)
In [ ]:
divide(15, 0)
Note that the finally clause is executed no matter what, even if the return
statement has already executed!
This makes it useful for cleanup tasks, such as closing an open file, restoring a state, or something along those lines.
Here is the most difficult piece of this lecture: handling semantic errors. This is the situation where your program runs, but doesn't produce the correct result. These errors are commonly known as bugs, and the process of correcting the bugs is debugging.
There are three main methods commonly used for debugging Python code. In order of increasing sophistication, they are:
print
statementspdb
In [ ]:
def entropy(p):
p = np.asarray(p) # convert p to array if necessary
items = p * np.log(p)
return -np.sum(items)
Say these are our probabilities:
In [ ]:
p = np.arange(5.)
p /= p.sum()
In [ ]:
entropy(p)
We get nan
, which stands for "Not a Number". What's going on here?
Often the first thing to try is to simply print things and see what's going on. Within the file, you can add some print statements in key places:
In [ ]:
def entropy(p):
p = np.asarray(p) # convert p to array if necessary
print(p)
items = p * np.log(p)
print(items)
return -np.sum(items)
entropy(p)
By printing some of the intermediate items, we see the problem: 0 * np.log(0)
is resulting in a NaN
. Though mathematically it's true that $\lim_{x\to 0} [x\log(x)] = 0$, the fact that we're performing the computation numerically means that we don't obtain this result.
Often, inserting a few print statements can be enough to figure out what's going on.
In [ ]:
%%file test_script.py
import numpy as np
def entropy(p):
p = np.asarray(p) # convert p to array if necessary
items = p * np.log(p)
import IPython; IPython.embed()
return -np.sum(items)
p = np.arange(5.)
p /= p.sum()
entropy(p)
Now open a terminal and run this. You'll see that an IPython interpreter opens, and from there you can print p
, print items
, and do any manipulation you feel like doing. This can also be a nice way to debug a script.
Python comes with a built-in debugger called pdb. It allows you to step line-by-line through a computation and examine what's happening at each step. Note that this should probably be your last resort in tracing down a bug. I've probably used it a dozen times or so in five years of coding. But it can be a useful tool to have in your toolbelt.
You can use the debugger by inserting the line
import pdb; pdb.set_trace()
within your script. Let's try this out:
In [ ]:
def entropy(p):
import pdb; pdb.set_trace()
p = np.asarray(p) # convert p to array if necessary
items = p * np.log(p)
return -np.sum(items)
entropy(p)
This can be a more convenient way to debug programs and step through the actual execution.
When you run this, you'll see the pdb prompt where you can enter one of several commands. If you type h
for "help", it will list the possible commands:
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pp run unt
a c continue exit l q s until
alias cl d h list quit step up
args clear debug help n r tbreak w
b commands disable ignore next restart u whatis
break condition down j p return unalias where
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv
Type h
collowed by a command to see the documentation of that command:
(Pdb) h n
n(ext)
Continue execution until the next line in the current function
is reached or it returns.
The most useful are probably the following:
q
(uit): quit the debugger and the program.c
(ontinute): quit the debugger, continue in the program.n
(ext): go to the next step of the program.list
: show the current location in the file.<enter>
: repeat the previous command.p
(rint): print variables.s
(tep into): step into a subroutine.r
(eturn out): return out of a subroutine.We'll see more of this in the next section.
In [ ]:
%%file numbers.dat
123 456 789
And we want to execute the following function:
In [ ]:
def add_lines(filename):
f = open(filename)
lines = f.read().split()
f.close()
result = 0
for line in lines:
result += line
return result
filename = 'numbers.dat'
total = add_lines(filename)
print(total)
We get a type error. We can immediately open the debugger using IPython's %debug
magic function. Remember to type q
to quit!
In [ ]:
%debug
We see that we need to convert the line to an integer before adding!