IACS Computes! 2019

IACS Computes! High School summer camp

Binder

Day 1

Day 2

Day 3

Day 4

Day 5

Day 6

Day 7

Day 8

Day 9

View the Project on GitHub harpolea/IACS_computes_2019

Errors

I’m sure by now you’ve encountered an error or two when trying to run your programs. Up until now, an error would generally cause Python to cease execution of the program and print some kind of complaint to the screen, which is sometimes what we want the program to do, and sometimes not. These error messages can themselves be used to find out what is wrong with your program. When you encounter an error, don’t panic. The best thing to do is to read the text of the error message and see if you can find any clues as to what went wrong in your code. After that, if you can’t find it, try searching the internet (stackoverflow.com in particular is very useful!). Nowadays, for just about every error you encounter, somebody else will encountered it before and written about it online.

However, sometimes the code we write will produce an error but we wish the program to continue running anyway. Sometimes your code will be broken in such a way that doesn’t produce an error message (but it will give you the wrong answer). This can be particularly difficult to debug.

Below we’ll learn some ways to produce our own error messages, and describe ways we can work with errors so that our programs only break when we want them to.

Assert statements

Probably the simplest way for a programmer to generate an error on their own is through an assert statement. The assert keyword takes an expression and checks to see if that expression evaluates to True. If it does, then the program is allowed to continue. If not, it raises an exception (essentially an error) and terminates the program.

Let’s take a look at this simple function which takes two lists, multiplies them together element-by-element, and adds up the result.

def dot(a, b):             # find out how long the list a is
    answer = 0             # initialize a variable to hold our answer
    for x, y in zip(a, b): # loop over the lists
        answer += x * y    # multiply each matching element of a and b, and add to answer
    return answer          # return the answer

Can you think of a reason this that function might behave unexpectedly? What would happen if the lists had different lengths? Well, if a is shorter than b, (or b shorter than a), this function will still give an answer without us ever knowing something is wrong.

def dot(a, b):
    assert len(a) == len(b) # Make sure the two lists have equal length
    answer = 0              # initialize a variable to hold our answer
    for x, y in zip(a, b):  # loop over the lists
        answer += x * y     # multiply each matching element of a and b, and add to answer
    return answer           # return the answer
dot([1,2,3],[4,5,6])
32
dot([1,2,3,3,3,3,3],[4,5,6])
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-4-f077d6d949c0> in <module>
----> 1 dot([1,2,3,3,3,3,3],[4,5,6])


<ipython-input-2-16660ae540a0> in dot(a, b)
      1 def dot(a, b):
----> 2     assert len(a) == len(b) # Make sure the two lists have equal length
      3     answer = 0              # initialize a variable to hold our answer
      4     for x, y in zip(a, b):  # loop over the lists
      5         answer += x * y     # multiply each matching element of a and b, and add to answer


AssertionError: 
dot([1,2],[4,5,6])
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-5-02c8af38ad61> in <module>
----> 1 dot([1,2],[4,5,6])


<ipython-input-2-16660ae540a0> in dot(a, b)
      1 def dot(a, b):
----> 2     assert len(a) == len(b) # Make sure the two lists have equal length
      3     answer = 0              # initialize a variable to hold our answer
      4     for x, y in zip(a, b):  # loop over the lists
      5         answer += x * y     # multiply each matching element of a and b, and add to answer


AssertionError: 

We can make our assert statement more useful by putting in a message that will be displayed if the assertion fails:

def dot(a,b):
    assert len(a)==len(b) , "The lists given are not the same length."
    answer = 0
    for x, y in zip(a, b):
        answer += x * y
    return answer
dot([1,2,3,3,3,3,3],[4,5,6])
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-7-f077d6d949c0> in <module>
----> 1 dot([1,2,3,3,3,3,3],[4,5,6])


<ipython-input-6-b7a3605a328f> in dot(a, b)
      1 def dot(a,b):
----> 2     assert len(a)==len(b) , "The lists given are not the same length."
      3     answer = 0
      4     for x, y in zip(a, b):
      5         answer += x * y


AssertionError: The lists given are not the same length.
dot([1,2,3],[4,5,6])
32

Try blocks

Sometimes you can predict what kind of errors your code might run into, and immediately know how to fix it. Remember when we tried to write a function that solves the quadratic equation? We ran into a problem that sometimes our quadratic would have complex solutions (solutions with an imaginary part). We could just define a function that always uses complex numbers, but using complex numbers where they’re not needed isn’t really optimal. While this can be solved using an if statement, for the sake of demonstration, let’s use a try block to fix this problem. Here’s the new version of our quadratic solver:

import math
import cmath # "complex math"
def solver(a,b,c):
    try:
        x1 = (-b + math.sqrt(b**2 - 4 * a * c)) / (2 * a)
        x2 = (-b - math.sqrt(b**2 - 4 * a * c)) / (2 * a)
    except ValueError:
        x1 = (-b + cmath.sqrt(b**2 - 4 * a * c)) / (2 * a)
        x2 = (-b - cmath.sqrt(b**2 - 4 * a * c)) / (2 * a)
    print(x1, x2)
solver(2, 1, 1)
(-0.25+0.6614378277661477j) (-0.25-0.6614378277661477j)

The above function will work for both real and complex answers! And if the answer is real, it won’t use complex numbers (causing that unsightly +0j in the solution).

Let’s go over the try block in detail. The code first tries to run the code under the try keyword. If it succeeds (i.e. no exceptions occur), then it continues on to the print statement. However, if an exception occurs (as would happen if math.sqrt was given a negative number), it looks to see if we’ve given it a fall-back option in case that exception should occur, in the form of an except statement. To do this we write except and then the name of the error we’re expecting (math.sqrt issues a ValueError if it encounters a negative number). The actual naming of the error is optional. If you just write except:, then the code below it will be exectued if any exception occurs. You can even list multiple types of exceptions in a single except statement if you wish.

There are two more possible keywords that can be used to create blocks beneath a try block: the else keyword, and the finally keyword. They are not very common as they have very specific uses, but for any who are interested, I’ll give a brief summary here. Anything in an else block of code will only be executed if the initial try block executed successfully (and not if the except block is what executed). Exceptions raised in the else block will not be caught by the except statement, which may be a good thing if you only want the except block to catch a very specific exception.

The finally keyword is used to create a block of code that is executed whether or not an exception is raised. If an exception is raised that is caught by an except block, the finally block will run after the except block. If an exception is raised that is not caught by an except block, then the finally block will run before the exception is reported. In this way, it’s sometimes used to ‘sneak in’ code between the generation of an exception and the termination of the code execution.

Practice

Data validation

Write a function that takes a list of numbers, calculates the mean and returns it.

# your code here 

def mean(a_list):
    
    try:
        assert type(a_list) == list, "Input must be a list"

        for element in a_list:
            assert type(element) == int or type(element) == float, "Input must be a list of numbers"
            
    except AssertionError:
        return 0
    
    return sum(a_list) / len(a_list)
# your code here 

def mean(a_list):
    
    try:
        return sum(a_list) / len(a_list)
            
    except TypeError:
        return 0
    
    
mean("hello")
0
mean([4,8,1,6,9,2,7,9])
5.75
mean([True, False, True, True])
0
mean(('one', 'two', 'three', 'four'))
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-9-d6f2dbbb48a2> in <module>
----> 1 mean(('one', 'two', 'three', 'four'))


<ipython-input-2-5b020dd3398a> in mean(a_list)
      3 def mean(a_list):
      4 
----> 5     return sum(a_list) / len(a_list)


TypeError: unsupported operand type(s) for +: 'int' and 'str'

Now try running your function with the following input arguments:

You should find that your function does not work for all of them. So although originally when you wrote your function you nay have intended for the user to only ever pass in a string of numbers as the arguments, in practice because python is quite chill when it comes to checking data types, it is possible for someone to try running your function on completely inapproriate things.

Modify your function to validate (check) the data before you try doing any calculations with it. You could do this using assert statements (e.g. to check that the input variable is a list and that all the items in the list are numbers). You could have a try/except block in there so that if something does go wrong, your function will exit gracefully rather than returning you a (perhaps confusing) error message.


Back to day 5