wk3.0

Warm-up

  • The four compass points can be abbreviated by single-letter strings as “N”, “E”, “S”, and “W”. Write a function turn_clockwise that takes one of these four compass points as its parameter, and returns the next compass point in the clockwise direction. Here are some tests that should pass:
assert turn_clockwise("N") == "E"
assert turn_clockwise("W") == "N"

In [ ]:
def turn_clockwise(direction):
    compass = {"N":"E" , "E": "S", "S":"W", "W":"N"}
    return compass[direction]

assert turn_clockwise("N") == "E"
assert turn_clockwise("W") == "N"

turn_clockwise("N")
  • You might ask “What if the argument to the function is some other value?” For all other cases, the function should return the value None:
assert turn_clockwise(42) == None
assert turn_clockwise("rubbish") == None

In [ ]:
def turn_clockwise(direction):
    compass = {"N":"E" , "E": "S", "S":"W", "W":"N"}
    try:
        return compass[direction]
    except KeyError:
        print("That's not a direction ya dingus!")
        return None

try:
    turn_clockwise()
except KeyError:
    print("Enter a direction")
"""
assert turn_clockwise() == None
assert turn_clockwise(47) == None
assert turn_clockwise("rubbish") == None
"""
  • Write a function day_name that converts an integer number 0 to 6 into the name of a day. Assume day 0 is “Sunday”. Once again, return None if the arguments to the function are not valid. Here are some tests that should pass:
assert day_name(3) == "Wednesday"
assert day_name(6) == "Saturday"
assert day_name(42) == None

In [ ]:
def day_name(num):
    days = ("Sun", "Mon", "Tues", "Wed", "Thur")
    try:
        return days[num]
    except:
        return None

assert day_name(3) == "Wed"
assert day_name(0) == "Sun"
assert day_name(42) == None
day_name(2)
  • Write the inverse function day_num which is given a day name, and returns its number:
assert day_num("Friday") == 5
assert day_num("Sunday") == 0
assert day_num(day_name(3)) == 3
assert day_name(day_num("Thursday")) == "Thursday"

In [ ]:
days = ("Sun", "Mon", "Tues", "Wed", "Thur")

days.index("Mon")

lst = []
for day in days:
    lst.append((days.index(day), day) )

dct = dict(lst)
dct
  • Once again, if this function is given an invalid argument, it should return None:
assert day_num("Halloween") == None
  • Write a function that helps answer questions like ‘“Today is Wednesday. I leave on holiday in 19 days time. What day will that be?”’ So the function must take a day name and a delta argument — the number of days to add — and should return the resulting day name:
assert day_add("Monday", 4) ==  "Friday"
assert day_add("Tuesday", 0) == "Tuesday"
test(day_add("Tuesday", 14) == "Tuesday"
test(day_add("Sunday", 100) == "Tuesday"

Hint: use the first two functions written above to help you write this one.


In [ ]:
def day_name(num):
    """Takes in day index and returns day name"""
    days = ("Sun", "Mon", "Tues", "Wed", "Thur", "Fri", 
            "Sat")   
    try:
        return days[num]
    except:
        return None

def day_num(day):
    days = ("Sun", "Mon", "Tues", "Wed", "Thur", "Fri", 
            "Sat")
    try:
        return days.index(day)
    except:
        return None

In [ ]:
def day_add(day, delta):
    number = (day_num(day) + delta) % 7
    return day_name(number)

In [ ]:
assert day_name(3) == "Wed"
assert day_name(0) == "Sun"
assert day_name(42) == None

assert day_num("Fri") == 5
assert day_num("Sun") == 0
assert day_num(day_name(3)) == 3

assert day_add("Tues", 0) == "Tues"
assert day_add("Fri", 3) == "Mon"

assert day_add("Fri", -2) == "Wed"

In [ ]:
-1%7
  • Can your day_add function already work with negative deltas? For example, -1 would be yesterday, or -7 would be a week ago:
    assert day_add("Sunday", -1) == "Saturday"
    assert day_add("Sunday", -7) == "Sunday"
    assert day_add("Tuesday", -100) == "Sunday"
  • If your function already works, explain why. If it does not work, make it work. Hint: Play with some cases of using the modulus function % (introduced at the beginning of the previous chapter). Specifically, explore what happens to x % 7 when x is negative.
  • Write a function days_in_month which takes the name of a month, and returns the number of days in the month. Ignore leap years:
    assert days_in_month("February") == 28
    assert days_in_month("December") == 31
    If the function is given invalid arguments, it should return None.

Write a function to_secs that converts hours, minutes and seconds to a total number of seconds. Here are some tests that should pass:

assert to_secs(2, 30, 10) == 9010
assert to_secs(2, 0, 0) == 7200
assert to_secs(0, 2, 0) == 120
assert to_secs(0, 0, 42) == 42
assert to_secs(0, -10, 10) == -590
  • Extend to_secs so that it can cope with real values as inputs. It should always return an integer number of seconds (truncated towards zero):
    assert to_secs(2.5, 0, 10.71) == 9010
    assert to_secs(2.433,0,0) == 8758
  • Write three functions that are the “inverses” of to_secs:
    • hours_in returns the whole integer number of hours represented by a total number of seconds.
    • minutes_in returns the whole integer number of left over minutes in a total number of seconds, once the hours have been taken out.
    • seconds_in returns the left over seconds represented by a total number of seconds. You may assume that the total number of seconds passed to these functions is an integer. Here are some test cases:
      assert hours_in(9010) == 2
      assert minutes_in(9010) == 30
      assert seconds_in(9010) == 10

Fruitful functions

  • temporary variables

In [ ]:
def mult(x1, x2, x3, x4):
    multi = x1*x2*x3*x4
    add = x1 + x2 + x3 + x4
    return multi*add

mult(1,1,1,2)

In [ ]:
%quickref
  • dead code, or unreachable code

In [ ]:
def bad():
    print("Hi")
    return "bye"
    
bad()
  • Make sure that your code accesses the whole range of input. Ex.
    def bad_absolute_value(x):
      if x < 0:
          return -x
      elif x > 0:
          return x

In [ ]:
def bad_absolute_value(x):
    if x <= 0:
        return -x
    elif x > 0:
        return x
    
bad_absolute_value(0)

Sometimes sticking a return in a for loop is a good idea:


In [ ]:


In [ ]:
def find_first_2_letter_word(xs):
    """ Returns the first two letter word in a list. If no two letter word exists, returns an empty string"""
    for index, wd in enumerate(xs):
        if len(wd) == 2:
            
            return (wd, index)
    return ("", index)

print('res1', find_first_2_letter_word(["This",  "is", "a", "dead", "parrot"]))

print('res2', find_first_2_letter_word(["I",  "like",  "cheese", "bah"]))

Incremental development

The key aspects of the process are:

  1. Start with a working skeleton program and make small incremental changes. At any point, if there is an error, you will know exactly where it is.
  2. Use temporary variables to refer to intermediate values so that you can easily inspect and check them.
  3. Once the program is working, relax, sit back, and play around with your options. (There is interesting research that links “playfulness” to better understanding, better learning, more enjoyment, and a more positive mindset about what you can achieve — so spend some time fiddling around!) You might want to consolidate multiple statements into one bigger compound expression, or rename the variables you’ve used, or see if you can make the function shorter. A good guideline is to aim for making code as easy as possible for others to read.

Debugging

Another powerful technique for debugging (an alternative to single-stepping and inspection of program variables), is to insert extra print functions in carefully selected places in your code. Then, by inspecting the output of the program, you can check whether the algorithm is doing what you expect it to. Be clear about the following, however:

  • You must have a clear solution to the problem, and must know what should happen before you can debug a program. Work on solving the problem on a piece of paper (perhaps using a flowchart to record the steps you take) before you concern yourself with writing code. Writing a program doesn’t solve the problem — it simply automates the manual steps you would take. So first make sure you have a pen-and-paper manual solution that works. Programming then is about making those manual steps happen automatically.

  • Do not write chatterbox functions. A chatterbox is a fruitful function that, in addition to its primary task, also asks the user for input, or prints output, when it would be more useful if it simply shut up and did its work quietly.

For example, we’ve seen built-in functions like range, max and abs. None of these would be useful building blocks for other programs if they prompted the user for input, or printed their results while they performed their tasks. So a good tip is to avoid calling print and input functions inside fruitful functions, unless the primary purpose of your function is to perform input and output. The one exception to this rule might be to temporarily sprinkle some calls to print into your code to help debug and understand what is happening when the code runs, but these will then be removed once you get things working.

Composition


In [ ]:
day_name(day_num("Wed"))

Boolean functions for test hiding


In [ ]:
def tester(line):
    tests a bunch of stuff
    if all the stuff is good:
        return True
    else:
        return False

def main_func(emails):
    for line in emails:
        if tester(line):
            return line

Lecture 2

Modules

Random numbers

A few uses of random numbers:

  • To play a game of chance where the computer needs to throw some dice, pick a number, or flip a coin,
  • To shuffle a deck of playing cards randomly,
  • To allow/make an enemy spaceship appear at a random location and start shooting at the player,
  • To simulate possible rainfall when we make a computerized model for estimating the environmental impact of building a dam,
  • For encrypting banking sessions on the Internet.

In [7]:
import random

# Create a black box object that generates random numbers
rng = random.Random()

dice_throw = rng.randrange(1,7)   # Return an int, one of 1,2,3,4,5,6
delay_in_seconds = rng.random() * 5.0

print('dice_throw', dice_throw)
print('delay_in_seconds', delay_in_seconds)


dice_throw 1
delay_in_seconds 3.901082468780449

How would we get odd numbers between 1 and 100 (exclusive)?


In [15]:
for num in range(10):
    print(rng.randrange(1,100,2))


27
17
25
99
37
5
83
65
97
77
  • random.Random() returns a uniform distribution.

  • There are other distributions as well.


In [27]:
random.random() # returns a number in interval [0,1). We need to scale it!

for num in range(10):
    print(random.random())


0.26271360781722963
0.8888887777027128
0.7607591003741009
0.10139091507892262
0.3321042528686271
0.2559371250627922
0.41050844394473796
0.21251634227842542
0.2742334808717193
0.7451143296453842

In [32]:
cards = list(range(52))  # Generate ints [0 .. 51]
                         #    representing a pack of cards.
rng.shuffle(cards)       # Shuffle the pack

cards


Out[32]:
[47,
 46,
 29,
 39,
 15,
 24,
 23,
 48,
 50,
 8,
 0,
 16,
 38,
 13,
 44,
 37,
 26,
 31,
 27,
 19,
 10,
 21,
 18,
 30,
 4,
 36,
 51,
 11,
 40,
 5,
 25,
 22,
 49,
 42,
 41,
 35,
 14,
 1,
 33,
 7,
 20,
 3,
 2,
 12,
 9,
 17,
 6,
 34,
 43,
 45,
 28,
 32]

Repeatability and Testing

  • deterministic algorithm
  • pseudo-random generators

In [44]:
drng = random.Random(15)  # Create generator with known starting state

for n in range(10):
    print(drng.randint(1,100)) # Always 7.


27
2
67
95
5
21
31
3
8
88

Picking balls from bags, throwing dice, shuffling a pack of cards


In [47]:
import random

def make_random_ints(num, lower_bound, upper_bound):
   """
     Generate a list containing num random ints between lower_bound
     and upper_bound. upper_bound is an open bound.
   """
   rng = random.Random()  # Create a random number generator
   result = []
   for i in range(num):
      result.append(rng.randrange(lower_bound, upper_bound))
   return result

make_random_ints(4, 3,10)


Out[47]:
[3, 7, 9, 7]

In [48]:
make_random_ints(5, 1, 13)  # Pick 5 random month numbers


Out[48]:
[9, 6, 3, 9, 4]

Getting unique values


In [49]:
xs = list(range(1,13))  # Make list 1..12  (there are no duplicates)
rng = random.Random()   # Make a random number generator
rng.shuffle(xs)         # Shuffle the list
result = xs[:5]         # Take the first five elements

In [50]:
result


Out[50]:
[7, 1, 3, 2, 12]

The 'shuffle and slice' method is okay for small numbers but would not be so great if you only wanted a few elements, but from a very large domain.

Suppose I wanted five numbers between one and ten million, without duplicates. Generating a list of ten million items, shuffling it, and then slicing off the first five would be a performance disaster! So let us have another try:


In [55]:
import random

def make_random_ints_no_dups(num, lower_bound, upper_bound):
    """
     Generate a list containing num random ints between
     lower_bound and upper_bound. upper_bound is an open bound.
     The result list cannot contain duplicates.
    """
    result = []
    rng = random.Random()
    for i in range(num):
        while True:
            candidate = rng.randrange(lower_bound, upper_bound)
            if candidate not in result:
                break
        result.append(candidate)
    return result

xs = make_random_ints_no_dups(5, 1, 10000000)
print(xs)


[2855989, 4158489, 5250594, 9186164, 8877894]

In [54]:
"""
def make_random_ints_no_dups(num, lower_bound, upper_bound):
    """
     Generate a list containing num random ints between
     lower_bound and upper_bound. upper_bound is an open bound.
     The result list cannot contain duplicates.
    """
    result = []
    rng = random.Random()
    while len(result) < num:
        candidate = rng.randrange(lower_bound, upper_bound)
        if candidate in result:
            continue
        result.append(candidate)
    return result

make_random_ints_no_dups(5, 1, 1000)
"""


Out[54]:
[723, 499, 251, 936, 411]

This method is okay but still has some problems.

Can you see what's going to happen in the next case?


In [ ]:
xs = make_random_ints_no_dups(10, 1, 6) # Yikes!

The time module

Looking at code efficiency


In [61]:
from timeit import default_timer as timer

t1 = timer()
print("hi")
t2 = timer()

print(t2 - t1)


hi
0.00015378399984911084

In [62]:
from timeit import default_timer as timer

def do_my_sum(xs):
    sum = 0
    for v in xs:
        sum += v
    return sum

sz = 10000000        # Lets have 10 million elements in the list
testdata = range(sz)

t0 = timer()
my_result = do_my_sum(testdata)
t1 = timer()
print("my_result    = {0} (time taken = {1:.4f} seconds)"
        .format(my_result, t1-t0))

t2 = timer()
their_result = sum(testdata)
t3 = timer()
print("their_result = {0} (time taken = {1:.4f} seconds)"
        .format(their_result, t3-t2))


my_result    = 49999995000000 (time taken = 1.0098 seconds)
their_result = 49999995000000 (time taken = 0.3839 seconds)

In [ ]:
def do_my_sum(xs):
    sum = 0
    for v in xs:
        sum += v
    return sum

sz = 10000000        # Lets have 10 million elements in the list
testdata = range(sz)

In [63]:
%%timeit

my_result = do_my_sum(testdata)


1 loops, best of 3: 962 ms per loop

In [64]:
%%timeit

their_result = sum(testdata)


1 loops, best of 3: 364 ms per loop

Creating your own modules

  • Save as a script and import!
  • The init.py file.

Namespaces

  • Each function, script, system has its own namespace.

Scope and lookup rules

  • The scope of an identifier is the region of program code in which the identifier can be accessed, or used.

There are three important scopes in Python:

  • Local scope refers to identifiers declared within a function. These identifiers are kept in the namespace that belongs to the function, and each function has its own namespace.
  • Global scope refers to all the identifiers declared within the current module, or file.
  • Built-in scope refers to all the identifiers built into Python — those like range and min that can be used without having to import anything, and are (almost) always available.

Python (like most other computer languages) uses precedence rules: the same name could occur in more than one of these scopes, but the innermost, or local scope, will always take precedence over the global scope, and the global scope always gets used in preference to the built-in scope. Let’s start with a simple example:


In [65]:
def range(n): 
    return 123*n

print(range(10)) # What will this print?


1230

In [66]:
n = 10
m = 3
def f(n):
    m = 7
    return 2*n+m

print(f(5), n, m) # What about this one?


17 10 3

Now we know why we use a return in our functions: to pass between namespaces!

Attributes and the dot operator

Variables defined inside a module are called attributes of the module. We’ve seen that objects have attributes too: for example, most objects have a doc attribute, some functions have a annotations attribute. Attributes are accessed using the dot operator (.).

Three import statement variants


In [ ]:
import math
x = math.sqrt(10)

In [ ]:
from math import cos, sin, sqrt
x = sqrt(10)

In [ ]:
from math import *   # Import all the identifiers from math,
                     #   adding them to the current namespace.
x = sqrt(10)         # Use them without qualification.

In [ ]:
# Here's a freebie since I like you guys
import math as m
m.pi

In [ ]:
def area(radius):
    import math
    return math.pi * radius * radius

x = math.sqrt(10)      # This gives an error

Exercises:

  1. Open help for the calendar module.

    • Try the following:
      import calendar
      cal = calendar.TextCalendar()      # Create an instance
      cal.pryear(2012)                   # What happens here?
    • Observe that the week starts on Monday. An adventurous CompSci student believes that it is better mental chunking to have his week start on Thursday, because then there are only two working days to the weekend, and every week has a break in the middle. Read the documentation for TextCalendar, and see how you can help him print a calendar that suits his needs.

    • Find a function to print just the month in which your birthday occurs this year.

    • Try this:
      d = calendar.LocaleTextCalendar(6, "SPANISH")
      d.pryear(2012)
    • Try a few other languages, including one that doesn’t work, and see what happens.
    • Experiment with calendar.isleap. What does it expect as an argument? What does it return as a result? What kind of a function is this?
  • Make detailed notes about what you learned from these exercises.
  • Open help for the math module.
  • How many functions are in the math module?
  • What does math.ceil do? What about math.floor? (hint: both floor and ceil expect floating point arguments.)
  • Describe how we have been computing the same value as math.sqrt without using the math module.
  • What are the two data constants in the math module?
  • Record detailed notes of your investigation in this exercise.
  • Investigate the copy module. What does deepcopy do? In which exercises from last chapter would deepcopy have come in handy?
  • Create a module named mymodule1.py. Add attributes myage set to your current age, and year set to the current year. Create another module named mymodule2.py. Add attributes myage set to 0, and year set to the year you were born. Now create a file named namespace_test.py. Import both of the modules above and write the following statement:
    print( (mymodule2.myage - mymodule1.myage) ==
         (mymodule2.year - mymodule1.year)  )
  • When you will run namespace_test.py you will see either True or False as output depending on whether or not you’ve already had your birthday this year.

  • What this example illustrates is that out different modules can both have attributes named myage and year. Because they’re in different namespaces, they don’t clash with one another. When we write namespace_test.py, we fully qualify exactly which variable year or myage we are referring to.

  • Add the following statement to mymodule1.py, mymodule2.py, and namespace_test.py from the previous exercise:

    print("My name is", __name__)
  • Run namespace_test.py. What happens? Why? Now add the following to the bottom of mymodule1.py:
    if __name__ == "__main__":
      print("This won't run if I'm  imported.")
  • Run mymodule1.py and namespace_test.py again. In which case do you see the new print statement? In a Python shell / interactive interpreter, try the following:
    >>> import this
    What does Tim Peters have to say about namespaces?
    Give the Python interpreter’s response to each of the following from a continuous interpreter session:
    >>> s = "If we took the bones out, it wouldn't be crunchy, would it?"
    >>> s.split()
    >>> type(s.split())
    >>> s.split("o")
    >>> s.split("i")
    >>> "0".join(s.split("o"))
    Be sure you understand why you get each result. Then apply what you have learned to fill in the body of the function below using the split and join methods of str objects: ``` def myreplace(old, new, s): """ Replace all occurrences of old with new in s. """ ...

assert myreplace(",", ";", "this, that, and some other thing") == "this; that; and some other thing") assert myreplace(" ", "", "Words will now be separated by stars.") == "Wordswillnowbeseparatedby**stars.") ``` Your solution should pass the tests.