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 [3]:
X = [1, 2, 3
a = 4
In [ ]:
y = 4*x + 3
In [4]:
def f():
return GARBAGE
No error is generated by defining this function. The error doesn't occur until the function is called.
In [5]:
f()
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 [6]:
a = 'aaa'
# blah blah blah
x = 1 + int(a)
print (x)
In [7]:
X = 1 / 0
In [11]:
import numpy as np
np.sum([1, 2, 3, 4])
Out[11]:
In [10]:
np.sum?
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 [ ]:
total = 0
for k in ks:
total += (-3.0) ** -k / (2 * k + 1)
In [18]:
from math import sqrt
def approx_pi(nterms=100):
ks = np.arange(nterms)
ans = sqrt(12) * np.sum([-3.0 ** -k / (2 * k + 1) for k in ks])
if ans < 1:
import pdb; pdb.set_trace()
return ans
approx_pi(1000)
Out[18]:
Looks OK, yes? Let's try it out:
In [ ]:
approx_pi(1000)
Huh. That doesn't look like $\pi$. Maybe we need more terms?
In [ ]:
k = 2
(-3.0) ** -k / (2 * k + 1)
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 [24]:
try:
print("this block gets executed first")
GARBAGE
except (ValueError, RuntimeError) as err:
print("this block gets executed if there's an error")
print (err)
print ("I am done!")
In [ ]:
def f(x):
if isinstance(x, int) or isinstance(x, float):
return 1.0/x
else:
raise ValueError("argument must be an int.")
In [ ]:
f(0)
In [ ]:
def f(x):
try:
return 1.0/x
except (TypeError, ZeroDivisionError):
raise ValueError("Argument must be a non-zero number.")
In [ ]:
f(0)
In [ ]:
f('aa')
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 [26]:
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, 3)
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 result4o-
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.")
Write exception code that handles all exceptions except ZeroDivideError
In [ ]:
x = 0
excpt = None
try:
1.0/x
except Exception as err:
if isinstance(err, ZeroDivisionError):
pass
else:
raise Exception("Got error.")
In [ ]:
type(excpt)
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)