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)