Week 7 : Lab A

Test, log, debug and optimize

CS1P - University of Glasgow - John H. Williamson - 2019/2020 -- Python 3.x

Lab exercise

You must submit a reasonble attempt at this exercise to gain a tick for this work.

Note that Part B is in a separate Moodle download.

Remember to save your work frequently!

Purpose of this lab

This lab will get you up to speed on:

  • reading and writing simple logs
  • writing tests with assert
  • profiling and optimising code
  • using the ipdb debugger

Before the lab

  • Attempt at least the A exercises.

In [2]:
## RUN THIS CELL FIRST!
%load_ext autoreload
%autoreload 2

from utils.tick import tick
from utils.test_utils import should_pass, should_fail
from utils.test_utils import reset_tracking, track, verify_track
from utils.complexity import time_complexity            
from IPython.core.debugger import set_trace
print("All loaded OK")


All loaded OK

A.1 Assertions

Write a function test_list(l) which uses assert to check if a list l satisfies all of these constraints:

  • the list is non-empty
  • the list contains only strings
  • every string is nonempty
  • every string has only lower case letters in it
  • every string is unique
  • the length of the list is even

In [24]:
# Solution goes here
def test_list(l):
    assert(len(l) != 0)
    assert(all(isinstance(s, str) for s in l))
    assert(all(s.strip() for s in l))
    assert(all(s.isalpha() and s == s.lower() for s in l))
    assert(len(l) == len(set(l)))
    assert(len(l) % 2 == 0)

In [25]:
## Tests
with tick():
    should_fail(test_list, [])
    should_fail(test_list, [""])
    should_fail(test_list, ["a"])
    should_fail(test_list, [1, "a"])
    should_fail(test_list, [1, 2])
    should_fail(test_list, ["a", "b", "c"])
    should_fail(test_list, ["alpha", ""])
    should_fail(test_list, ["a", "b", "c", "a"])
    should_fail(test_list, ["a", "a"])
    should_fail(test_list, ["a", "b", "3", "."])
    should_fail(test_list, ["A", "B", "C", "D"])
    should_fail(test_list, ["alpha", "Bravo", "charlIe", "delta"])
    should_fail(test_list, ["a", "b", "c", ""])

with tick():
    should_pass(test_list, ["a", "b"])
    should_pass(test_list, ["alpha", "bravo"])
    should_pass(test_list, ["alpha", "bravo", "charlie", "d"])
    should_pass(test_list, ["ale", "base", "cheap", "liqour"])
    should_pass(test_list, ["ale", "base", "cheap", "liqour", "twenty", "thirty"])


✓ Correct

✓ Correct

A.2 Time complexity

The function below runs in constant time -- it doesn't depend on the value of n and takes the same each time it is called regardless. The time_complexity() call shows this as a graph, and gives scores, which should show the time complexity is most likely to be constant (it will have the highest score printed).


In [30]:
def constant_time(n):
    i = n 
    return i


# number=30: run the function 30 times and average the result
# ns= range(1, 1000, 50): set n to values from 1 to 1000, stepping by 50 each time
# reps=50: repeat the whole experiment 50 times
time_complexity(constant_time, ns=range(5, 1000, 50), number=30, reps=50);


/Users/cajetan/Downloads/week_7_lab_a/utils/complexity.py:21: RuntimeWarning: overflow encountered in square
  return np.sum((x*fn(ns) - ts)**2)
/usr/local/lib/python3.7/site-packages/scipy/optimize/optimize.py:1974: RuntimeWarning: invalid value encountered in double_scalars
  tmp1 = (x - w) * (fx - fv)
/usr/local/lib/python3.7/site-packages/scipy/optimize/optimize.py:1975: RuntimeWarning: invalid value encountered in double_scalars
  tmp2 = (x - v) * (fx - fw)
Scores for constant_time
  constant     53.1%
  log          18.2%
  linear        8.4%
  nlogn         7.9%
  quadratic     6.6%
  cubic         5.9%
  exp           0.0%
  factorial     0.0%

Task Using nested loops write functions which take a parameter n and do computations which will run in:

  • linear_time(n) which runs in O(N) linear time
  • quadratic_time(n) which runs in O(N^2) quadratic time
  • cubic_time(n) which runs in O(N^3) cubic time

Plot graphs for each, using time_complexity. Use number=20, ns=range(5, 100, 5), reps=15


In [41]:
def linear_time(n):
    x = 0
    for i in range(n):
        x += n
    return x
        

def quadratic_time(n):
    x = 0
    for i in range(n):
        for j in range(n):
            x += n
    return x

def cubic_time(n):
    x = 0
    for i in range(n):
        for j in range(n):
            for k in range(n):
                x += n
    return x

In [42]:
## Tests
with tick():
    _, _, scores = time_complexity(linear_time, ns=range(5, 100, 3), number=5, plot=False, reps=40);
    assert scores["linear"]>0.2 

with tick():
    _, _, scores = time_complexity(quadratic_time, ns=range(5, 100, 3), number=5, plot=False, reps=40);
    assert scores["quadratic"]>0.5

with tick():    
    _, _, scores = time_complexity(cubic_time, ns=range(5, 100, 3), number=5, plot=False, reps=40);
    assert scores["cubic"]>0.5


✓ Correct

✓ Correct

✓ Correct

A.2B (OPTIONAL EXTENSION)

Write a function that runs in O(N log N) time, and plot the time complexity graph. (hint: how long does x.sorted() take for a list of n random items x?)


In [3]:
# Solution goes here
import random

def nlogn_function(n):
    items = [random.randint(0, 100) for i in range(n)]
    return sorted(items)
            
time_complexity(nlogn_function, ns=range(5, 1000, 50), number=30, reps=50);


/Users/cajetan/Downloads/week_7_lab_a/utils/complexity.py:21: RuntimeWarning: overflow encountered in square
  return np.sum((x*fn(ns) - ts)**2)
/usr/local/lib/python3.7/site-packages/scipy/optimize/optimize.py:1974: RuntimeWarning: invalid value encountered in double_scalars
  tmp1 = (x - w) * (fx - fv)
/usr/local/lib/python3.7/site-packages/scipy/optimize/optimize.py:1975: RuntimeWarning: invalid value encountered in double_scalars
  tmp2 = (x - v) * (fx - fw)
Scores for nlogn_function
  linear       65.7%
  nlogn        22.7%
  quadratic     4.2%
  log           2.7%
  cubic         2.6%
  constant      2.0%
  exp           0.0%
  factorial     0.0%

A.3 Visualise recursion with logging

Many people find it hard to visualise what is going on in recursion. Use the principles of logging to add print statements that make a trace of what happens when the recursive function below is called.

Try and print out the results nicely; you might want to add additional parameters to the function to make logging nicer.


In [4]:
## This function returns the ordered permutations of a sequence

def permutations(l, perm=[]):
    if len(l) == 0: 
        return [perm]
    
    results = []
    for i in range(len(l)):        
        results += permutations(l[:i] + l[i+1:], perm + [l[i]])
    return results

permutations("abc")


Out[4]:
[['a', 'b', 'c'],
 ['a', 'c', 'b'],
 ['b', 'a', 'c'],
 ['b', 'c', 'a'],
 ['c', 'a', 'b'],
 ['c', 'b', 'a']]

In [12]:
# Solution goes here
def permutations(l, perm=[]):
    print()
    print("----- START -----")
    print("Permuting: {}".format(l))
    
    if len(l) == 0: 
        print("Returning permutation {}".format([perm]))
        return [perm]
    
    results = []
    for i in range(len(l)):        
        results += permutations(l[:i] + l[i+1:], perm + [l[i]])
        print("Results: {}".format(results))
        
    print("----- END -----")
    print()
    return results

permutations("abc")


----- START -----
Permuting: abc

----- START -----
Permuting: bc

----- START -----
Permuting: c

----- START -----
Permuting: 
Returning permutation [['a', 'b', 'c']]
Results: [['a', 'b', 'c']]
----- END -----

Results: [['a', 'b', 'c']]

----- START -----
Permuting: b

----- START -----
Permuting: 
Returning permutation [['a', 'c', 'b']]
Results: [['a', 'c', 'b']]
----- END -----

Results: [['a', 'b', 'c'], ['a', 'c', 'b']]
----- END -----

Results: [['a', 'b', 'c'], ['a', 'c', 'b']]

----- START -----
Permuting: ac

----- START -----
Permuting: c

----- START -----
Permuting: 
Returning permutation [['b', 'a', 'c']]
Results: [['b', 'a', 'c']]
----- END -----

Results: [['b', 'a', 'c']]

----- START -----
Permuting: a

----- START -----
Permuting: 
Returning permutation [['b', 'c', 'a']]
Results: [['b', 'c', 'a']]
----- END -----

Results: [['b', 'a', 'c'], ['b', 'c', 'a']]
----- END -----

Results: [['a', 'b', 'c'], ['a', 'c', 'b'], ['b', 'a', 'c'], ['b', 'c', 'a']]

----- START -----
Permuting: ab

----- START -----
Permuting: b

----- START -----
Permuting: 
Returning permutation [['c', 'a', 'b']]
Results: [['c', 'a', 'b']]
----- END -----

Results: [['c', 'a', 'b']]

----- START -----
Permuting: a

----- START -----
Permuting: 
Returning permutation [['c', 'b', 'a']]
Results: [['c', 'b', 'a']]
----- END -----

Results: [['c', 'a', 'b'], ['c', 'b', 'a']]
----- END -----

Results: [['a', 'b', 'c'], ['a', 'c', 'b'], ['b', 'a', 'c'], ['b', 'c', 'a'], ['c', 'a', 'b'], ['c', 'b', 'a']]
----- END -----

Out[12]:
[['a', 'b', 'c'],
 ['a', 'c', 'b'],
 ['b', 'a', 'c'],
 ['b', 'c', 'a'],
 ['c', 'a', 'b'],
 ['c', 'b', 'a']]

A.4 Test driven development

Image: "DSC_8226" by huguet92 is licensed under CC BY-NC-ND 2.0

The code below is supposed to do something with colours. The hexify_colour function doesn't work and you don't know exactly what the code is supposed to do. It was written by a junior developer and has a lot of issues, even though it looks basically correct.

But you do have tests which check what the results should be. Using these tests:

  • work out what the code should be doing
  • fix it so it does the right thing

In [47]:
def hexify_color(r,g=None,b=None):
    if (g is None and b is not None) or (g is not None and b is None):
        raise Exception()
    
    if r < 0.0:
        r = 0.0
    elif r > 1.0:
        r = 1.0
        
    if g is None or b is None:
        g = r
        b = r
    elif g < 0.0:
        g = 0.0
    elif g > 1.0:
        g = 1.0
    
    if b < 0.0:
        b = 0.0
    elif b > 1.0:
        b = 1.0
    
    r_int = int(r*255)
    g_int = int(g*255)
    b_int = int(b*255)    
    
    return "#{r:02x}{g:02x}{b:02x}".format(r=r_int, g=g_int, b=b_int)

print(hexify_color(-1.0, 0.0, 1.0))


#0000ff

In [48]:
## Tests
with tick():
    assert hexify_color(0.0, 0.0, 0.0) == '#000000'
    assert hexify_color(0.5, 0.5, 0.5) == '#7f7f7f'
    assert hexify_color(1.0, 1.0, 1.0) == '#ffffff'
    assert hexify_color(1.0, 0.0, 0.0) == '#ff0000'
    assert hexify_color(0.0, 1.0, 0.0) == '#00ff00'
    assert hexify_color(0.0, 0.0, 1.0) == '#0000ff'
    assert hexify_color(1.0) == '#ffffff'
    assert hexify_color(0.0) == '#000000'
    assert hexify_color(-1.0, 0.0, 1.0) == '#0000ff'
    assert hexify_color(-1.0, 0.0, 10.0) == '#0000ff'
    assert hexify_color(10.0, 10.0, 10.0) == '#ffffff'

    assert hexify_color(-1.0) == '#000000'
    assert hexify_color(0.5) == '#7f7f7f'

    should_fail(hexify_color, 0.5, 0.5)
    should_fail(hexify_color, 0.5, 0.5)


✓ Correct


In [12]:
# Solution goes here

A.4 Basic ipdb

The code below is a solution to Lab 3, A.4. Practice tracing through this code using the ipdb debugger. You should watch the video tutorial on ipdb (Week 7's video) before attempting this part.

Here's a quick reference to ipdb commands:

Basic debugger commands

The ipdb debugger can either take single letter commands or the full text.

  • s step execute the next line, and enter any function that is called
  • n next execute the next line, and skip over any function that is called
  • c continue resume running until the next breakpoint is hit
  • return continue until the end of the current function
  • r run restart the program
  • l list print out the code corresponding to the current execution point
  • u/d up/down in the call stack
  • p print print out a value (e.g. p my_list to print out the current value of my_list)
  • a args print out the arguments that were used in the call to the current function
  • q quit YOU MUST QUIT THE DEBUGGER BEFORE ANY OTHER CODE WILL RUN!
  • b breakpoint set a new breakpoint on the given line number (e.g. b 20)

You can pre-insert breakpoints where the debugger will stop and wait for input by writing set_trace() in the code.


Instructions

  1. Before you start: Click on the cell, and press ESC-L to enable line numbers

  2. Add a breakpoint using set_trace() at the very start of all_rotates

  3. Run the cell; ipdb will start.
  4. You can use help at any time to get a list of commands inside the debugger
  5. Print out the argument to all_rotates
  6. Use list to see where you are in the code
  7. Step into the for loop
  8. Step into rotate() in the first iteration
  9. Use up to see where the call to rotate came from
  10. Use down to go back into rotate
  11. Then, in the next iteration, skip over the call to rotate (hint: use next)
  12. Then, set a breakpoint in the debugger at the second for loop
  13. Continue to that breakpoint
  14. Check the value of rotates is what you expect
  15. Continue execution to the end of the cell
  16. Quit the debugger using quit

In [86]:
def rotate(s):
    rotated = s[1:] + s[0]
    return rotated

def all_rotates(s):
    set_trace()
    rotates = []
    for i in range(len(s)):
        rotates.append(s)
        s = rotate(s)
        
    for rotated in rotates:
        print(rotated)
    
                  
all_rotates("steel")


> <ipython-input-86-4432e27b8270>(7)all_rotates()
      5 def all_rotates(s):
      6     set_trace()
----> 7     rotates = []
      8     for i in range(len(s)):
      9         rotates.append(s)

ipdb> exit
---------------------------------------------------------------------------
BdbQuit                                   Traceback (most recent call last)
<ipython-input-86-4432e27b8270> in <module>
     14 
     15 
---> 16 all_rotates("steel")

<ipython-input-86-4432e27b8270> in all_rotates(s)
      5 def all_rotates(s):
      6     set_trace()
----> 7     rotates = []
      8     for i in range(len(s)):
      9         rotates.append(s)

<ipython-input-86-4432e27b8270> in all_rotates(s)
      5 def all_rotates(s):
      6     set_trace()
----> 7     rotates = []
      8     for i in range(len(s)):
      9         rotates.append(s)

/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/bdb.py in trace_dispatch(self, frame, event, arg)
     86             return # None
     87         if event == 'line':
---> 88             return self.dispatch_line(frame)
     89         if event == 'call':
     90             return self.dispatch_call(frame, arg)

/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/bdb.py in dispatch_line(self, frame)
    111         if self.stop_here(frame) or self.break_here(frame):
    112             self.user_line(frame)
--> 113             if self.quitting: raise BdbQuit
    114         return self.trace_dispatch
    115 

BdbQuit: 

In [ ]:
# Solution goes here

A.5 Reading logs

The code below is supposed to take a string then convert it to Morse code. It does this by reading a file containing a Morse code table. There are several bugs, but there are logging messages provided to help you understand what is going on. Use the log messages which are generated to work out what the problems are with this code is and fix the code so it works as described above, and passes the tests. Do not use the debugger, or add additional logging statements to the code.

A Morse key, from the days when keyboards had just the one key. Image: by Brad Wilmot CC-NC-ND

In [70]:
def read_table(fname):
    morse_table = {}
    for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
        morse_table[letter] = ""
    print("{n} blank entries in the Morse table".format(n=len(morse_table)))
    fname = "morse.txt"
    print("Reading morse table from file '{fname}'".format(fname=fname))
    with open(fname) as f:
        for line in f:
            letter, code = line.split(" ")
            morse_table = {letter: code}
            print("Mapping {letter} to {code}".format(letter=letter, code=code))

    print("{n} entries in Morse table after loading".format(n=len(morse_table)))
    print()
    return morse_table


def convert_to_morse(s):
    print()
    morse_table = read_table("data/morse.txt")
    output = []
    for ch in s:
        upper_ch = ch.upper()
        if upper_ch in morse_table:
            print("Converting {ch} to {code}".format(ch=ch, code=morse_table[upper_ch]))
            output.append(morse_table[ch.upper()])
        else:
            output.append("   ")
        output.append(" ")
    result = " ".join(output)
    print("Conversion result is {result}".format(result=result))
    return result

In [84]:
# Solution goes here
def read_table(fname):
    morse_table = {}
    for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
        morse_table[letter] = ""
    print("{n} blank entries in the Morse table".format(n=len(morse_table)))
    fname = "data/morse.txt"
    print("Reading morse table from file '{fname}'".format(fname=fname))
    with open(fname) as f:
        for line in f:
            letter, code = line.strip().split(" ")
            morse_table[letter.upper()] = code
            print("Mapping {letter} to {code}".format(letter=letter.upper(), code=code))

    print("{n} entries in Morse table after loading".format(n=len(morse_table)))
    print()
    return morse_table


def convert_to_morse(s):
    print()
    morse_table = read_table("data/morse.txt")
    output = []
    for ch in s:
        upper_ch = ch.upper()
        if upper_ch in morse_table:
            print("Converting {ch} to {code}".format(ch=ch, code=morse_table[upper_ch]))
            output.append(morse_table[ch.upper()])
        else:
            output.append("   ")
            
    result = " ".join(output)
    print("Conversion result is {result}".format(result=result))
    return result

In [85]:
with tick():
    assert convert_to_morse('sos')=='... --- ...'
    assert convert_to_morse('hello there')=='.... . .-.. .-.. ---     - .... . .-. .'
    assert convert_to_morse('sphinx of black quartz judge my vow') == '... .--. .... .. -. -..-     --- ..-.     -... .-.. .- -.-. -.-     --.- ..- .- .-. - --..     .--- ..- -.. --. .     -- -.--     ...- --- .--'


26 blank entries in the Morse table
Reading morse table from file 'data/morse.txt'
Mapping A to .-
Mapping B to -...
Mapping C to -.-.
Mapping D to -..
Mapping E to .
Mapping F to ..-.
Mapping G to --.
Mapping H to ....
Mapping I to ..
Mapping J to .---
Mapping K to -.-
Mapping L to .-..
Mapping M to --
Mapping N to -.
Mapping O to ---
Mapping P to .--.
Mapping Q to --.-
Mapping R to .-.
Mapping S to ...
Mapping T to -
Mapping U to ..-
Mapping V to ...-
Mapping W to .--
Mapping X to -..-
Mapping Y to -.--
Mapping Z to --..
26 entries in Morse table after loading

Converting s to ...
Converting o to ---
Converting s to ...
Conversion result is ... --- ...

26 blank entries in the Morse table
Reading morse table from file 'data/morse.txt'
Mapping A to .-
Mapping B to -...
Mapping C to -.-.
Mapping D to -..
Mapping E to .
Mapping F to ..-.
Mapping G to --.
Mapping H to ....
Mapping I to ..
Mapping J to .---
Mapping K to -.-
Mapping L to .-..
Mapping M to --
Mapping N to -.
Mapping O to ---
Mapping P to .--.
Mapping Q to --.-
Mapping R to .-.
Mapping S to ...
Mapping T to -
Mapping U to ..-
Mapping V to ...-
Mapping W to .--
Mapping X to -..-
Mapping Y to -.--
Mapping Z to --..
26 entries in Morse table after loading

Converting h to ....
Converting e to .
Converting l to .-..
Converting l to .-..
Converting o to ---
Converting t to -
Converting h to ....
Converting e to .
Converting r to .-.
Converting e to .
Conversion result is .... . .-.. .-.. ---     - .... . .-. .

26 blank entries in the Morse table
Reading morse table from file 'data/morse.txt'
Mapping A to .-
Mapping B to -...
Mapping C to -.-.
Mapping D to -..
Mapping E to .
Mapping F to ..-.
Mapping G to --.
Mapping H to ....
Mapping I to ..
Mapping J to .---
Mapping K to -.-
Mapping L to .-..
Mapping M to --
Mapping N to -.
Mapping O to ---
Mapping P to .--.
Mapping Q to --.-
Mapping R to .-.
Mapping S to ...
Mapping T to -
Mapping U to ..-
Mapping V to ...-
Mapping W to .--
Mapping X to -..-
Mapping Y to -.--
Mapping Z to --..
26 entries in Morse table after loading

Converting s to ...
Converting p to .--.
Converting h to ....
Converting i to ..
Converting n to -.
Converting x to -..-
Converting o to ---
Converting f to ..-.
Converting b to -...
Converting l to .-..
Converting a to .-
Converting c to -.-.
Converting k to -.-
Converting q to --.-
Converting u to ..-
Converting a to .-
Converting r to .-.
Converting t to -
Converting z to --..
Converting j to .---
Converting u to ..-
Converting d to -..
Converting g to --.
Converting e to .
Converting m to --
Converting y to -.--
Converting v to ...-
Converting o to ---
Converting w to .--
Conversion result is ... .--. .... .. -. -..-     --- ..-.     -... .-.. .- -.-. -.-     --.- ..- .- .-. - --..     .--- ..- -.. --. .     -- -.--     ...- --- .--

✓ Correct

# Part B is an a separate file -- download from Moodle when available

In [ ]: