Closed note, closed computer, 120 minutes, 165 points.
The median score was 140/165, aka 85%.
# CS106A Exam Reference Reminder
# [square brackets] denote functions that we have not used heavily
# which typically do not appear on exams.
Bit:
bit = Bit(filename)
bit.front_clear() bit.left_clear() bit.right_clear()
bit.get_color()
bit.move() bit.left() bit.right()
bit.paint('red') [bit.erase()]
General functions:
len() int() str() float() range() list() abs() sum()
sorted(lst, key=lambda, reverse=False) min() max()
# min() max() also take optional key=lambda
String functions:
isalpha() isdigit() isupper() islower()
find() upper() lower() split() strip()
startswith() endswith() [replace() join()]
List functions:
append() index() map() [extend() pop()]
Dict functions:
keys() values() items()
File functions
with open(filename) as f: # with form
for line in f: # loop over lines
Lambda:
lambda n: 2 * n
SimpleImage:
# read filename
image = SimpleImage(filename)
# create blank image
image = SimpleImage.blank(width, height)
# foreach loop
for pixel in image:
# range/y/x loop
for y in range(image.height):
for x in range(image.width):
pixel = image.get_pixel(x, y)
Grid 2D:
grid = Grid.build([['row', '0'], ['row', '1']])
grid.width, grid.height - properties
grid.in_bounds(x, y) - True if in bounds
grid.get(x, y) - returns contents at that x,y, or None if empty
grid.set(x, y, value) - sets new value into grid at x,y
Each of the following Python expressions evaluates to a value without error. Write the resulting Python value in the box below each expression.
>>> 85 // 10
>>> 31 % 5
>>> s = 'Winter' >>> s[1] + s[3:]
One-Liners For each part, write a 1-line expression to compute the indicated value with: map() or sorted() or min() or max() or a comprehension. You do not need to call list() for these.
# a. Given a list of int values, compute a list where # each value is multiplied * 10 >>> nums = [3, 4, -20, 10] # yields: [30, 40, -200, 100]
# b. Given a list of (x, y, z) tuples of numbers. # write am expression to extract the tuple # with the smallest z value >>> tuples = [(1, 2, 3), (3, 1, 5), (2, 3, 2)] # yields: (2, 3, 2)
# c. Given a list of numbers.
# write am expression to produce a list with each
# number in string form surrounded by '(' ')'
>>> nums = [3, 1, 4, 1]
# yields: ['(3)', '(1)', '(4)', '(1)']
a. Given an int n. If n is negative, return a list length 1 containing n. For example if n is -6, return [-6]. Otherwise, return a list of n numbers following the pattern [100, 99, 98, ...]
nums(-4) -> [-4] nums(4) -> [100, 99, 98, 97] def nums(n):
b. Given a string s and a non-negative int n.
If n is a valid index in s, then return the
substring of s from its beginning up to and
including the char at index n.
Otherwise, return s with 'NOPE' added at its end.
substring_n('abcd', 2) -> 'abc'
substring_n('abcd', 0) -> 'a'
substring_n('abcd', 4) -> 'abcdNOPE'
def substring_n(s, n):
c. Given a string s, and exclaim, a list of chars.
Build and return a string of only the alphabetic
chars in s. Each char in the result that is
in the exclaim list should have a '!' added after
it in the result.
str_alpha('12abc$', ['x', 'b']) -> 'ab!c'
def str_alpha(s, exclaim):
a. Find the first '#' in s.
Ignore the char immediately after the '#'.
If the char after the ignored char is 'a',
and the char after that is a digit, then return
an int value as follows: treat the 'a' as the
digit '2'. Combining the '2' with the digit following
makes a 2-digit number. Return the int value of this number
doubled. So for example 'xx#xa3x' returns 46.
In all other cases return -1.
two_digit('xx#xa3x') -> 46
two_digit('xx#xx5x') -> -1
two_digit('xx#') -> -1
def two_digit(s):
b. Given a string s.
Construct and and return a list
of the int index numbers of any '*'
chars in s. For example, the string
'ab*cd*' returns [2, 5], since index 2 and index 5 are the '*' chars in s. Suggestion: loop over the index numbers of s.
find_stars('ab*cd*') -> [2, 5]
find_stars('***') -> [0, 1, 2]
find_stars('abc') -> []
def find_stars(s):
c. Given a string s. Find the first '@@@' in s. If present,
return a version of s with the substrings before and after the '@@@' swapped,
and the '@@@' changed to a '$'.
For example, 'abc@@@12' returns '12$abc'. In all other cases, return the original s unchanged.
swap_at('abc@@@12') -> '12$abc'
swap_at('abc@@12') -> 'abc@@12'
def swap_at(s):
d. (This function makes use of the previous functions, and you can get full credit for this function independent of your code for the previous functions.)
Given a string s. Use find_stars() to find the locations of any '*' in s, and use these
locations to compute the result.
If there are exactly 2 '*', then take the substring between the '*', not including the '*' themselves,
and return the result of passing this substring through the swap_at() function.
If there is exactly 1 '*', then return the substring strictly before the '*'.
In all other cases, return s unchanged.
enstar('xx*abc@@@12*xx') -> '12$abc'
enstar('xx*abc@12*xx') -> 'abc@12'
enstar('xx*abc@12') -> 'xx'
enstar('xxabc') -> 'xxabc' # typo: aka two_star()
def two_star(s):
Suppose we are doing a study of teen texts where they propose a bad idea. We'll say the bad idea is made of alphabetic and space ' ' chars leading up to the substring 'YOLO'. Find the first 'YOLO' in s using s.find(). Use a loop to find zero or more contiguous alphabetic/' ' chars preceding the 'YOLO' and return the whole thing. For example 'idea:eat million donuts YOLO #meh' returns 'eat million donuts YOLO'. If there is no 'YOLO' then return the empty string. If you need to write a long line of Python, you may mark the end of one line with "..." and continue on the next line on the paper, and we will understand.
'idea:eat million donuts YOLO #meh' -> 'eat million donuts YOLO' 'xx@a bYOLO$' -> 'a bYOLO' 'xyz' -> '' def bad_yolo(s):
We have a text file where each line is made of one or more words separated by commas. Additionally, "comment" lines of text may be present, marked by a '#' as the first char of the line. An example file:
Butter,Cork,Drill Zebra,Trick,Milk,FILLY # comment line will be ignored Pluck,Rhythm
Write code for a read_words(filename, target) function which reads through all the lines, and for each line goes through all the words. Do not process the words on each "comment" line. The target parameter is a non-empty string like 'ill'. The function should build and return a list of all the words from the file which include the target as a substring, not case-sensitive.
For example, with the target 'ill', the result from the above file should be the list ['Drill', 'FILLY']. With the target 'il', the result should be ['Drill', 'Milk', 'FILLY']. Note that 'will' from the comment line is not in any results.
The basic code to init the result list and read through the lines is provided.
def read_words(filename, target)
result = []
with open(filename) as f:
for line in f:
line = line.strip()
The draw_vals(canvas, width, height, vals) function takes four parameters: canvas is a canvas to draw on, and width, and height are the size of the canvas, and vals is a list of numbers to draw. Stripes 20 pixels high at the top and bottom edge of the canvas, and 20 pixels wide at the left and right edge of the canvas are left blank. The drawing area is the rectangle within these 4 blank stripes.
There will be 1 or more float numbers in the vals list, each in the range 0..212 inclusive. Draw a vertical line for each number in vals. Draw the first val as a vertical line at the leftmost pixel of the drawing area. Draw the second val one pixel to the right of the first, and so on. We'll assume the canvas is wide enough to hold all the vals. Each vertical line should have one end at the bottom edge of the drawing area. The length of each line should be proportionate to its value: when value is 0, the line should begin and end on the bottom edge of the drawing area. When val is 212, the line should extend all the way to the topmost pixel of the drawing area. Use canvas.draw_line(x1, y1, x2, y2) to draw each line.
def draw_vals(canvas, width, height, vals):
We have two collections of dog names: dogs is a list of dog names, and scores is a dict with dog name keys, and the value is a score for that name 1..100. The two data structures do not have the exact same names in them: there are names in the list that are not in the dict, and vice versa, as shown in this example:
dogs = ['woofers', 'pinky', 'spot', 'sprinkles']
scores = {
'woofers': 91, 'spot': 50, 'angel': 90,
'fido': 32, 'sprinkles': 95
}
Write code for a top_dogs(dogs, scores) function that takes in the dogs list and the scores dict. Create and return shorter version of the dogs list which contains only the dogs from the list which are in the scores dict and also have a score which is 90 or more. For example, with the above data the function should return: ['woofers', 'sprinkles']
def top_dogs(dogs, scores):
We are tracking data about airplane flights. For terminology, we'll say a flight leaves the depart airport and arrives at the arrive airport. We have a "fly" dict that has a key for each depart airport, e.g. 'sfo'. The value for each key is a list of flights, where each flight is a string like 'ua555-jfk-1720'. Each flight string has three data items separated by dash chars '-': the flight name ('ua555'), the arrive airport ('jfk'), and the depart time ('1720'). For this problem, we are only using the depart and arrive airports.
fly = {
'sfo': ['ua555-jfk-1720', 'u235-abq-0600', 'twa700-jfk-1157'],
'sjc': ['af122-abq-0812'],
}
Write code for a compute_arrives(fly) function that takes in the fly dict and computes and returns an arrives dict as follows: The arrives dict should have a key for each arrive airport mentioned across the flights. The value for each arrive should be a list of the airports that depart at least one flight to that arrive airport. A depart airport should be in the list at most once, so the code must avoid adding a duplicate airport to the list.
With the above dict as input, the function should return this arrives dict:
{'jfk': ['sfo'], 'abq': ['sfo', 'sjc']}
def compute_arrives(fly):
We have a dict keeping track of the highest loudness recorded at different dorms, separated by year. The "years" dict has a key for each year, and its value is a nested dict with a key per dorm, and its value is the max recorded loudness for that dorm in that year. The data is patchy, with some dorms missing in some years.
years = {2026: {'branner': 78.2, 'toyon': 90, 'flomo': 62.5},
2025: {'lagunita': 60, 'toyon': 87.4, 'roble': 88.1},
...
a. Write code for an add_loud(years, dorm, loud) function where years is the years dict, dorm is the dorm name (e.g. 'toyon'), and loud is the recorded loud number. Add the given data into the years dict. However, do not add the loud number if there is already a higher number recorded for that year and dorm. No return is needed. (typo: some copies did not have "year", and we announced the correction on the board before the exam.)
def add_loud(years, year, dorm, loud)
b. Write 2 Doctests testing the add_loud() function: one test where the loud number is added, and one test where the number is not added because of the louder-already-there rule. The parameters passed into the function can be minimal, so long a they show the needed pattern.
# Doctest syntax reminder with double_char(s) lecture example
>>> double_char('Hello')
'HHeelllloo'
Suppose you are working on the code for a system that uses robots to make deliveries. There is an "ops" dict which tracks the current operations, which has keys 'jobs' and 'likes'. The 'jobs' is a list of job dicts. Each job dict has a 'robot' key with a robot-id string like 'r971' that identifies the robot. The 'up' key is optional in a job. If present, it means the customer selected a form of "thumbs up" for that job. Finally, the 'likes' key has a dict where each key is a robot-id and its value is the cumulative total of likes for that robot.
ops = {'jobs': [ {'cust': 'c577', 'robot': 'r971', 'up': 3},
{'cust': 'c200', 'robot': 'r972'},
{'cust': 'c991', 'robot': 'r980', 'up': 2} ],
'likes': { 'r971': 1, 'r777': 5} }
Write code for the update_likes(ops) function, which transfers the 'up' data to 'likes' before the jobs are deleted, working as follows: Look at all the jobs. For each job with an 'up' key, add the value of the 'up' key to the corresponding robot-id entry in the 'likes' dict. Add the robot-id to the likes dict if necessary. No return is needed as the change is made inside the ops dict. For example, running update_likes(ops) on the above ops changes the likes dict to this:
{'r971': 4, 'r777': 5, 'r980': 2}
def update_likes(ops):
Suppose Somi Somi opens an on-campus store, and we end up with a flood of ice-cream like-score data for various users. We have many text lines of data, and each user can appear on multiple lines, so we want to create a summation dict that pulls all the data together. The lines look like:
katie:matcha:4^asha:chocolate^bob:mint:2 emily:mint:7^miguel:matcha:2 ...
The line is divided into groups by up-hat chars '^'. Each group is divided into parts by colon chars ':', like 'katie:matcha:4'. The first two strings in the group are the user and flavor, e.g. 'katie' and 'matcha'. The 3rd part, if present, is the int sentiment score to use. If it is not present, use a score of 1. There are no other extraneous up-hat or colon chars in the data. The data file has many lines, and users and flavors may appear on multiple lines.
Read through all the lines and create and return a "users" dict which has a key for each user in the data. The value for each user should be a nested "flavors" dict with a key for each flavor rated by that user, and its value is the sum of that user's scores for that flavor, like this:
users = {'katie': {'matcha': 6, 'vanilla': 5, 'mint': 6},
'asha': {'chocolate': 7, 'matcha': 2},
...
a. Write code to read in all the lines from the given filename, building and returning the users dict. The basic file-reading code is provided.
def read_users(filename):
users = {}
with open(filename) as f:
for line in f:
line = line.strip()
# Reminder of users structure
users = {'katie': {'matcha': 6, 'vanilla': 5, 'mint': 6},
'asha': {'chocolate': 7, 'matcha': 2},
...
b. Suppose we are curious about the sum of all the scores for a particular flavor, e.g. 'chocolate', summed up across all the users. Write code for a function flavor_sum(users, flavor) that takes the whole users dict and a single flavor, and returns the sum of all the scores for that flavor, across all the users. Return 0 if there are no scores for that flavor.
def flavor_sum(users, flavor):
c. Write code for a print_users(users) function that takes in the users dict produced by (a), and prints out all the users in sorted order, one per line. For each user, print out all its flavors in alphabetical order, each indented by one space and followed by its score, like this:
aaron chocolate 34 matcha 5 ... zeke chocolate 7 vanilla 5 def print_users(users):
#### 1. Short Answer
>>> 85 // 10 -> 8
>>> 31 % 5 -> 1
>>> s = 'Winter'
>>> s[1] + s[3:] -> 'iter'
# 1-liners
# mult * 10
[n * 10 for n in nums]
# smallest z tuple
min(tuples, key=lambda tup:tup[2])
# str form
['(' + str(n) + ')' for n in nums]
# or map()
map(lambda n: '(' + str(n) + ')', nums)
# or f'' string like hw8
[f'({n})' for n in nums]
#### 2. Warnmups
def nums(n):
if n < 0:
return [n] # this works, or could .append(n)
result = []
for i in range(n):
result.append(100 - i)
return result
def substring_n(s, n):
if n < len(s):
return s[:n + 1]
return s + 'NOPE'
def str_alpha(s, exclaim):
"""
>>> str_alpha('12abc$', ['x', 'b'])
'ab!c'
>>>
"""
result = ''
for ch in s:
if ch.isalpha():
result += ch
if ch in exclaim:
result += '!'
return result
#### 3. Strings
# (a)
def two_digit(s):
hash = s.find('#')
if hash == -1:
return -1
if hash + 3 < len(s): # chars after # are in bounds
# pattern: 'a' + digit
if s[hash + 2] == 'a' and s[hash + 3].isdigit():
two_digits = '2' + s[hash + 3]
return int(two_digits) * 2
return -1
# These 2 are relatively small, but they get used in (d)
# (b)
def find_stars(s):
result = []
for i in range(len(s)):
if s[i] == '*':
result.append(i)
return result
# (c)
def swap_at(s):
at = s.find('@@@')
if at == -1:
return s
return s[at + 3:] + '$' + s[:at]
# (d) - use helpers
def two_star(s):
stars = find_stars(s) # list like [2, 8]
if len(stars) == 2:
left = stars[0] # e.g. 2, Pull values into vars
right = stars[1]
between = s[left + 1:right]
return swap_at(between)
if len(stars) == 1:
return s[:stars[0]]
return s
#### 4. String YOLO
def bad_idea(s):
yolo = s.find('YOLO')
if yolo == -1:
return ''
# Move start towards start of string, over alpha and ' '
start = yolo - 1
while start >= 0 and (s[start].isalpha() or s[start] == ' '): # note ( )
start -= 1
return s[start + 1:yolo + 4]
#### 5. File / List
def read_words(filename, target):
result = []
with open(filename) as f:
for line in f:
line = line.strip()
# check that first char not '#'
# (given that lines are not-empty, or could check)
if line[0] != '#':
words = line.split(',')
# each word - check if target is in word
# convert to lowercase before checking
for word in words:
if target.lower() in word.lower():
result.append(word)
# improvement: target_low = target.lower() pre-loop
return result
#### 6. Drawing
def draw_vals(canvas, width, height, vals):
# Use for/i/range so have index number i for each val
for i in range(len(vals)):
x = 20 + i
y_bottom = (height - 1) - 20 # bottom of draw area
# Compute y_add as (fraction) * (max)
y_add = (val / 212) * (height - 40 - 1)
create_line(x, y_bottom, x, y_bottom + y_add)
#### 7. Dict-1 Dogs
def top_dogs(dogs, scores):
result = []
for dog in dogs:
if dog in scores and scores[dog] >= 90:
result.append(dog)
return result
#### 8. Dict-2 Flights
def compute_arrives(fly):
arrives = {}
for depart in fly.keys():
flights = fly[depart]
for flight in flights:
parts = flight.split('-')
arrive = parts[1] # 'jfk'
if arrive not in arrives:
arrives[arrive] = []
lst = arrives[arrive]
if depart not in lst:
lst.append(depart)
return arrives
### 9. Dict-3 Add-Loud
def add_loud(years, year, dorm, loud):
if year not in years: # didn't require this, due to question wording
years[year] = {}
dorms = years[year] # Var points to inner
# Add loud if dorm is not there, or if loud
# is greater than what's in there.
if dorm not in dorms:
dorms[dorm] = loud
elif loud > dorms[dorm]: # regular "if" works too
dorms[dorm] = loud
return years # returning years not required
# Doctests - need sufficient input parameters to get the result
# but we did not require showing the function result,
# though it is printed here
# a. Doctest where add happens - one way is passing {} as years.
# Or dorm could not be in years, or loud in years could be smaller.
>>> add_loud({}, 2026, 'lagunita', 90.0)
{2026: {'lagunita': 90.0}}
# Second Doctest where add does not happen - dorm has larger value
# already, so passed in 80.0 is not set.
>>> add_loud({2026: {'lagunita': 90.0}}, 2026, 'lagunita', 80.0)
{2026: {'lagunita': 90.0}}
#### 10. Dict Nesting
def update_likes(ops):
# Var points to inner
jobs = ops['jobs']
likes = ops['likes']
# loop over list of jobs
for job in jobs:
if 'up' in job:
# Pull data out of the job
robot = job['robot']
up = job['up']
if robot not in likes:
likes[robot] = 0
likes[robot] += up
return likes
#### 11 Capstone Somi Somi
def read_somi(filename):
users = {}
with open(filename) as f:
for line in f:
line = line.strip()
groups = line.split('^')
for group in groups:
parts = group.split(':')
user = parts[0]
flavor = parts[1]
score = 1 # default, or get from parts
if len(parts) == 3:
score = int(parts[2])
# put user/score into users
if user not in users:
users[user] = {}
flavors = users[user]
if flavor not in flavors:
flavors[flavor] = 0
flavors[flavor] += score
return users
def flavor_sum(users, flavor):
total = 0
for user in users.keys():
flavors = users[user]
if flavor in flavors:
total += flavors[flavor]
return total
def print_users(users):
for user in sorted(users.keys()):
print(user) # abby
flavors = users[user]
for flavor in sorted(flavors.keys()):
print(' ' + flavor, flavors[flavor]) # ' ' matcha 56