CS51A - Fall 2019 - Class 10

Example code in this lecture

   dictionaries.py
   call_stack.py
   recursion.py

Lecture notes

  • administrative
       - lunch with Dr. Dave or Prof Osborn
       - midterm next Thursday (10/10)
          - in class
          - paper-based
          - can bring in two pages of notes (either two pieces of paper, single-side or one piece, double-sided)
          - problems like practice problems
             - coding
             - what's wrong with this function
             - what would this function do
             - is this valid?
             - what would the output be
             - ...
          - practice writing code on paper (it's different than on the computer)
          - I'll post practice problems
          - Review for the first bit of lab tomorrow
          - cover everything through dictionaries (not recursion)

  • dictionaries (aka maps)
       - store keys and an associated value
          - each key is associated with a value
          - lookup can be done based on the key
          - this is a very common phenomena in the real world. What are some examples?
             - social security number
                - key = social security number
                - value = name, address, etc
             - phone numbers in your phone (and phone directories in general)
                - key = name
                - value = phone number
             - websites
                - key = url
                - value = location of the computer that hosts this website
             - car license plates
                - key = license plate number
                - value = owner, type of car, ...
             - flight information
                - key = flight number
                - value = departure city, destination city, time, ...
       - creating new dictionaries
          - dictionaries can be created using curly braces
             >>> d = {}
             >>> d
             {}
          - dictionaries function similarly to lists, except we can put things in ANY index and can use non-numerical indices
             >>> d[15] = 1
             >>> d
             {15: 1}
             
             - notice when a dictionary is printed out, we get the key AND the associated value

             >>> d[100] = 10
             >>> d
             {100: 10, 15: 1}
             >>> my_list = []
             >>> my_list[15] = 1
             Traceback (most recent call last):
              File "<string>", line 1, in <fragment>
             IndexError: list assignment index out of range

             - dictionaries ARE very different than lists....
          - we can also update the values already in a dictionary
             >> d[15] = 2
             >>> d
             {100: 10, 15: 2}
             >>> d[100] += 1
             >>> d
             {100: 11, 15: 2}
          - keys in the dictionary can be ANY *immutable* object
             >>> d2 = {}
             >>> >>> d2["dave"] = 1
             >>> d2["anna"] = 1
             >>> d2["anna"] = 2
             >>> d2["seymore"] = 100
             >>> d2
             {'seymore': 100, 'dave': 1, 'anna': 2}
          - the values can be ANY object
             - >>> d3 = {}
             >>> d3["dave"] = []
             >>> d3
             {'dave': []}
             >>> d3["dave"].append(1)
             >>> d3["dave"].append(40)
             >>> d3
             {'dave': [1, 40]}
          - be careful to put the key in the dictionary before trying to use it
             >>> d3["steve"]
             Traceback (most recent call last):
              File "<string>", line 1, in <fragment>
             KeyError: 'steve'
             >>> d3["steve"].append(1)
             Traceback (most recent call last):
              File "<string>", line 1, in <fragment>
             KeyError: 'steve'
          - how do you think we can create non-empty dictionaries from scratch?
             >>> another_dict = {"dave": 1, "anna":100, "seymore": 21}
             >>> another_dict
             {'seymore': 21, 'dave': 1, 'anna': 100}
          - what are some other methods you might want for dictionaries (things you might want to ask about them?
             - does it have a particular key?
             - how many key/value pairs are in the dictionary?
             - what are all of the values in the dictionary?
             - what are all of the keys in the dictionary?
             - remove all of the items in the dictionary?
          - dictionaries support most of the other things you'd expect them too that we've seen in other data structures
             >>> "seymore" in another_dict
             True
             >>> len(another_dict)
             3
          - dictionaries are a class of objects, just like everything else we've seen (called dict ... short for dictionary)
             >>> help(dict)
          - some of the more relevant methods:
             >>> d2
             {'seymore': 100, 'dave': 1, 'anna': 2}
             >>> d2.values()
             [100, 1, 2]
             >>> d2.keys()
             dict_keys(['seymore', 'dave', 'anna'])
             >>> d2.pop('seymore')
             >>> d2
             {'dave': 1, 'anna': 2}
             >>> d2.clear()
             >>> d2
             {}

  • generating counts
       - We're going to use dictionaries to store counts like we did on paper
       - Write a function called get_counts that takes a list of numbers and returns a dictionary containing the counts of each of the numbers
       - Key idea:

          def get_counts(numbers):
             d = {}

             for num in numbers:
                # do something here

             return d

       - There are two cases we need to contend with:
          1) if the number isn't in the dictionary

             - In this case we need to add it with the value 1

                d[num] = 1

          2) if the number is in the dictionary

             - In this case, we just need to increment it

                d[num] = d[num] + 1

             which can also be written

                d[num] += 1

       - Look at the get_counts function in dictionaries.py code

       - We now can generate the counts from our file

       >>> data = read_numbers('numbers.txt')
       >>> data
       >>> [1, 2, 3, 2, 1, 1, 2, 6, 7, 8, 10, 1, 5, 5, 5, 3, 8, 6, 7, 6, 4, 1, 1, 2, 3, 1, 2, 3]
       >>> get_counts(data)
       {1: 7, 2: 5, 3: 4, 6: 3, 7: 2, 8: 2, 10: 1, 5: 3, 4: 1}

  • Iterating over dictionaries
       - We're almost to the point where we can find the most frequent value.
       - Next, we need to go through all of the values in the dictionary to find the most frequent one.

       - there are many ways we could iterate over the things in a dictionary
          - iterate over the values
          - iterate over the keys
          - iterate over the key/value pairs
       - which one is most common?
          - since lookups are done based on the keys, iterating over the keys is the most common
       - by default, if you say:

          for key in dictionary:
             ...

          key will get associated with each key in the dictionary.
       - once we have the key, we can use it to lookup the value associated with that key and do whatever we want with the pair
          for key in dictionary:
             value = dictionary[key]
             ..

       - look at the print_counts function in dictionaries.py code
          - "\t" is the tab character
          
          >>> data = read_numbers('numbers.txt')
          >>> counts = get_counts(data)
          >> print_counts(counts)
          1   7
          2   5
          3   4
          6   3
          7   2
          8   2
          10   1
          5   3
          4   1

          Notice that the keys are not in numerical order. In general, there's no guarantee about the ordering of the keys, only that you'll iterate over all of them.

  • look at the get_most_frequent_value function in dictionaries.py code
       - Looks very similar to the my_max function we wrote in lecture8 (http://www.cs.pomona.edu/~dkauchak/classes/cs51a/lectures/lecture8-sequences.html)
          - We keep a variable (max_value) that stores the largest value we've seen so far
             - We'll initialize it to -1 assuming that the numbers are all positive
             - See problem set 6 for a general solution
          - We then iterate through each of the key/value pairs in our dictionary
             - We compare the value (i.e. counts[key]) to the largest value we've seen so far
             - If it's larger, we update max_value
          - The only difference with my_max is that we want to return the *key* associated with the largest value
             - We need another variable (max_key) that stores this key
             - Whenever we update max_value, we also update max_key

          >>> data = read_numbers('numbers.txt')
          >>> get_most_frequent_value(data)
          1

  • It may also be useful to not only get the most frequent value, but also how frequent it is
       - Anytime you want to return more than one value from a function, a tuple is often a good option
       - Look at the get_most_frequent function in dictionaries.py code
          - only difference is that we return a tuple and also include the max_value

          >>> data = read_numbers('numbers.txt')
          >>> get_most_frequent(data)
          (1, 7)

  • the call stack
       - what is displayed if we call mystery(2, 3) in call_stack.py code?
          - The mystery number is: 15
       - why?

       - We can visualize each of these function calls:

       mystery(2, 3)
          |
          V
       "The mystery number is: " + c(2, 3)
                   |
                   V
                   b(6) - 1
                   |
                   V
                   6 + a()
                      |
                      V
                      10

       - Finally, when "a" returns its value then we can work our way back and get the final answer
          - a() returns 10
          - which allows b to now return 16
          - once c knows its call to b was 16, it returns 15
          - and finally, we can generate the string that the mystery function prints out
       
       - the way that the computer keeps track of all of this is called the "stack"
          - as functions are called, the stack grows
             - new function calls are added onto the stack
          - when functions finished, the stack shrinks
             - that function call is removed
             - its result is given to the next function on the stack (the function that called it)
       
       - you can actually see this stack (called the "call stack"), when you get an error, e.g. we change a to "return 10 + ''":

          - when we run it we see:

          >>> mystery(2,3)
          Traceback (most recent call last):
           Python Shell, prompt 2, line 1
           # Used internally for debug sandbox under external interpreter
           File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 11, in <module>
           print("The mystery number is: " + str(c(num1, num2)))
           File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 8, in <module>
           return b(num1 * num2) - 1
           File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 5, in <module>
           return num + a()
           File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 2, in <module>
           if __name__ == '__main__':
          builtins.TypeError: unsupported operand type(s) for +: 'int' and 'str'

          - it's a little bit cryptic, but if you look on the right, you see the call from mystery, to c, to b and finally to a

  • write a function called factorial that takes a single parameter and returns the factorial of that number
       >>> factorial(1)
       1
       >>> factorial(2)
       2
       >>> factorial(3)
       6
       >>> factorial(8)
       40320

       - look at factorial_iterative function in recursion.py
          - I'm guessing most people wrote it this way
          - does a loop from 2 up to n and multiplies the numbers
       - Another option is factorial_iterative2 in recursion.py
          - Here we did a range through n, so i goes from 0, 1, ..., n-1
          - In the body of the loop be multiply by i+1, i.e., by 1, 2, ..., n
       - could we have done it any other way?

  • recursion
       - a recursive function is defined with respect to itself
          - what does this mean?
             - somewhere inside the function, the function calls itself
             - just like any other function call
             - the recursive call should be on a "smaller" version of the problem
       - can we write factorial recursively?
          - key idea: try and break down the problem into some computation, plus a smaller subproblem that looks similar

             5! = 5 * 4 * 3 * 2 * 1
             5! = 5 * 4!

          - a first try:

             def factorial(n):
                return n * factorial(n-1)

          - what happens if we run this with say 5?
             5 * factorial(4)
                |
                V
                4 * factorial(3)
                   |
                   V
                   3 * factorial(2)
                      |
                      V
                      2 * factorial(1)
                         |
                         V
                         1 * factorial(0)
                            |
                            V
                            0 * factorial(-1)
                               ...

             - at some point we need to stop. this is called the "base case" for recursion
             - when is that?
                - when n = 1 (or if you want to account for 0!, at 0)
          - how can we change our function to do this?
       - look at factorial function in recursion.py code
          - first thing, check to see if we're at the base case (if n == 0)
             - if so, just return 1 (this lets 0! be 1)
          - otherwise, we fall into our recursive case:
             - n * factorial(n-1)