This week in section you are going to get more practice with dictionaries and drawing!
Dictionaries
Recipes
Inspired by an unhealthy amount of Netflix and quarantine cooking, Parth and Peter are opening up a bakery. To try and keep costs low, they want to automate as much of their inventory tracking as possible.
Thanks to 106A, they think they can use dictionaries to help.
All of the ingredients they have available will be stored in a pantry
dictionary, whose keys are ingredients and values are weights (Of course
they're using the metric system), like so:
pantry = {
'flour': 400,
'sugar': 300,
'salt': 10,
'chocolate': 150
}
Each recipe is also stored in a dictionary, like the following
uninspiring concoction:
recipe = {
'flour': 200,
'salt': 2.5
}
(As you can tell from the recipe above, it doesn't look like their bakery is going to do very well, but that's not important right now.)
Recipes and the pantry are first stored as files, where each key value
pair is on a different line, separated by ':: '. The pantry list and
recipe above would look like:
pantry.txt
flour:: 400
sugar:: 300
salt:: 10
chocolate:: 150
recipe.txt
flour:: 200
salt:: 2.5
Our goal is to write a few functions to help Parth and Peter run their bakery. Begin by implementing the following function:
def read_dict_from_file(filename):
"""
Takes in the name of a file containing a recipe or
pantry list and reads it into a dictionary.
An example doctest using the file above:
>>> read_dict_from_file('recipe.txt')
{'flour': 200, 'salt': 2.5}
"""
Once you've developed the infrastructure to construct recipe and pantry list dictionaries, implement the following functions:
def can_make(recipe, pantry):
"""
Given the contents of the pantry, returns a boolean indicating
whether or not it is possible to follow the recipe. Note that
the parameters to this function are dictionaries, and not
filenames. The pantry should not be modified in this function
"""
pass
def make_recipe(recipe, pantry):
"""
Given a recipe and a pantry with enough ingredients to make the recipe,
modify the contents of the pantry to remove as many quantities as the
recipe requires. You may modify the pantry in place, but return the modified
pantry in order to test the output using doctests.
# using the recipe and pantry defined above
>>> make_recipe(recipe, pantry)
{'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}
"""
pass
Note that make_recipe assumes the pantry is sufficient for the recipe
but this is not necessarily always the case; thus, every call to
make_recipe will need to be guarded by a call to can_make_recipe.
Finally, implement a main function, which first reads in a pantry file
from a user and then continuously asks for recipe filenames from the
user and either prints an error message if the pantry does not have
sufficient ingredients, or removes the ingredients from the pantry if it
does exist, printing the pantry afterwards.
A sample run of the program is below, which repeatedly tries to make the recipe above in the vain hope that someone will actually want to eat it. Note that your program should be able to accept any valid recipe filename, though.
$ python3 pantry_manager.py
Enter pantry filename: pantry.txt
What recipe should we bake next (Press enter to quit.)? recipe.txt
You can make that recipe! Your pantry now looks like this:
{'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}
What recipe should we bake next (Press enter to quit.)? recipe.txt
You can make that recipe! Your pantry now looks like this:
{'flour': 0, 'sugar': 300, 'salt': 5, 'chocolate': 150}
What recipe should we bake next (Press enter to quit.)? recipe.txt
You can't make that recipe.
What recipe should we bake next (Press enter to quit.)? User presses enter immediately
def read_dict_from_file(filename):
"""
Takes in the name of a file containing a recipe or
pantry list and reads it into a dictionary.
β
An example doctest using the file above:
>>> read_dict_from_file('recipe.txt')
{'flour': 200, 'salt': 2.5}
"""
# Create an empty dictionary
d = {}
β
with open(filename) as f:
# For each line in the file
for line in f:
# Split this line into a list at '::'
key_val = line.split(":: ")
# Add key:value pair of ingredient:weight to our dictionary
d[key_val[0]] = float(key_val[1])
β
return d
β
β
def can_make(recipe, pantry):
"""
Given the contents of the pantry, returns a boolean indicating
whether or not it is possible to follow the recipe. Note that
the parameters to this function are dictionaries, and not
filenames. The pantry should not be modified in this function
"""
# For each key in the recipe dictionary
for ingredient in recipe:
# Check if amount of ingredient in pantry is less than amount needed for recipe
if pantry.get(ingredient, 0) < recipe[ingredient]:
return False
# Return True after checking every ingredient
return True
β
β
def make_recipe(recipe, pantry):
"""
Given a recipe and a pantry with enough ingredients to make the recipe,
modify the contents of the pantry to remove as many quantities as the
recipe requires. You may modify the pantry in place, but return the modified
pantry in order to test the output using doctests.
β
# using the recipe and pantry defined above
>>> make_recipe(recipe, pantry)
{'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}
"""
# For each value in recipe dictionary
for ingredient in recipe:
# Subtract the needed amount from this ingredient in pantry
pantry[ingredient] -= recipe[ingredient]
return pantry
β
β
def main():
pantry = read_dict_from_file(PANTRY_FILENAME)
while True:
recipe_filename = input(
"What recipe should we bake next (Press enter to quit.)? ")
if recipe_filename == "":
break
recipe = read_dict_from_file(recipe_filename)
if can_make(recipe, pantry):
make_recipe(recipe, pantry)
print("You can make that recipe! Your pantry now looks like this:")
print(pantry)
else:
print("You can't make that recipe.")
Cryptography
Cryptography is the study of techniques for communicating messages secretly. Imagine that Alice and Bob want to send messages to each other, but Eve can snoop on the messages they're sending and read them. Alice wants to figure out way to "encrypt" her messages so that if Eve reads the message, she won't be able to understand it, but Bob will be able to "decrypt" the message. We're going to write a program to help Alice and Bob do this.
Alice decides that she'll replace every letter in her original message
with a different letter. She's defined a variable in the program called
ENCRYPTION_DICT that keeps track of these associations:
ENCRYPTION_DICT = {
'A': 'T',
'B': 'H',
'C': 'E',
'D': 'Q',
'E': 'U',
'F': 'I',
'G': 'C',
'H': 'K',
...
}
Alice and Bob exchanged this dictionary, so they both know that this is the strategy, but Eve doesn't know that! Note that in order to avoid ambiguity, the values in this dictionary are unique (that is, each letter is a value in the dictionary exactly once).
Encryption
To start us off, implement the following function:
def encrypt(plaintext):
"""
Takes in plaintext as an input and returns 'ciphertext': the result
of substituting each letter in the plaintext by its corresponding
encrypted character in ENCRYPTION_DICT.
The plaintext comprises entirely of uppercase letters and non-alphabetic
characters like punctuation. Non-alphabetic characters needn't be encrypted,
but rather should appear in the plaintext in their original form.
>>> encrypt("HEY, HOW'S IT GOING?")
"KUD, KXZ'S BV CXBFC?"
>>> encrypt("I LOVE CS 106A!")
'B WXLU ES 106T!'
>>> encrypt("UNICORNS ARE THE MOST BEAUTIFUL ANIMALS IN EXISTENCE")
'AFBEXPFS TPU VKU NXSV HUTAVBIAW TFBNTWS BF UYBSVUFEU'
"""
Decryption
Now that Bob has received Alice's encrypted message, he needs to "decrypt" it, or convert it back to the original message. To help him do so, implement the following function:
def decrypt(ciphertext):
"""
Uses ENCRYPTION_DICT to decrypt each of the alphabetic characters of
ciphertext.
>>> decrypt("KUD, KXZ'S BV CXBFC?")
"HEY, HOW'S IT GOING?"
>>> decrypt('B WXLU ES 106T!')
'I LOVE CS 106A!'
>>> decrypt('AFBEXPFS TPU VKU NXSV HUTAVBIAW TFBNTWS BF UYBSVUFEU')
'UNICORNS ARE THE MOST BEAUTIFUL ANIMALS IN EXISTENCE'
"""
Note that in order to successfully decrypt a message, you need the
'reverse' of ENCRYPTION_DICT: rather than associating plaintext
characters with their encrypted counterparts, we need to go the other
way.
We suggest writing a function reverse_encryption_dict to decompose out
this problem. In class, we saw an example of this wherein a reversed
dictionary associated keys with lists of values, because multiple keys
can share the same value. Note that this doesn't apply to the case of
ENCRYPTION_DICT, because each character is guaranteed to have a unique
character. How does this affect how you implement
reverse_encryption_dict?
This is an interesting encryption scheme (known formally as a substitution cipher), but unfortunately isn't very secure. What issues do you see with it?
def encrypt(plaintext):
"""
Takes in plaintext as an input and returns 'ciphertext': the result
of substituting each letter in the plaintext by its corresponding
encrypted character in ENCRYPTION_DICT.
β
The plaintext comprises entirely of uppercase letters and non-alphabetic
characters like punctuation. Non-alphabetic characters needn't be encrypted,
but rather should appear in the plaintext in their original form.
"""
output = ""
β
# For each character in the unencrypted message
for ch in plaintext:
# If this is a character we have an encryption for, add the encryption
if ch in ENCRYPTION_DICT:
output += ENCRYPTION_DICT[ch]
# Otherwise, add the character in its original form
else:
output += ch
β
return output
β
β
def reverse_encryption_dict():
"""
This helper function returns a dictionary that is the reverse of
ENCRYPTION_DICT (keys and values are swapped).
"""
# Create a new dictionary
decryption_dict = {}
β
# For each plaintext character key in the encryption dictionary
for plaintext_char in ENCRYPTION_DICT:
# Get the encrypted character value for this key
encrypted_char = ENCRYPTION_DICT[plaintext_char]
# In the new dict, add the plaintext character as the value of our encrypted key
decryption_dict[encrypted_char] = plaintext_char
β
return decryption_dict
β
β
def decrypt(ciphertext):
"""
Uses ENCRYPTION_DICT to decrypt each of the alphabetic characters of
ciphertext.
"""
decryption_dict = reverse_encryption_dict()
output = ""
β
# For each character in the encrypted word
for ch in ciphertext:
# If this character can be decrypted, add its decryption to output string
if ch in decryption_dict:
output += decryption_dict[ch]
# Otherwise, add the original character to the output string
else:
output += ch
β
return output
Visualizing Big Tweets
In last week's section, you implemented a program that constructs what we refer to as a user tags dictionary. This dictionary maps twitter handles to dictionaries which map hashtags to frequencies of usage. For example, suppose we have the following user tags dictionary:
{'@kanyewest': {'yeezy': 50, 'chicago': 20, 'pablo': 30}}
This dictionary illustrates the unlikely scenario in which @kanyewest is the only user of twitter (since that is the only key in the outer dictionary) and that he has tweeted about yeezy, chicago and pablo 50, 20 and 30 times respectively.
Our goal is to implement a program to visualize this data for us, by showing the relative usage of each hashtag by a user. For the dictionary associated with @kanyewest, this visualization would look like this:

In order to produce this visualization, you should implement the following function:
def visualize_trends(canvas, user_tags, user_name):
"""
Draws a visualization of the top 10 hashtags used
by the user whose handle is user_name.
"""
As you write this function, keep in mind the following details:
- You need not worry about producing a user_tags dictionary. The starter code loads this dictionary in from a file and passes it into your function.
- In order to keep the bars a reasonable width, you need only plot the ten most frequently used hashtags (or fewer, if the user doesn't use many hashtags). We've provided a function get_top_tags, which accepts as a parameter a single user's hashtag frequency dictionary, and returns a frequency dictionary of just the TOP_N most used hashtags, where TOP_N is a constant defined to be equal to 10.
- All the bars should be equal width.
- All the bars should have a height that is proportional to their hashtag's usage. For example, in the example above, 20% of @kanyewest's tweets used the chicago hashtag, and so the bar for chicago is 20% the height of the canvas. Restated, the height of all the bars combined should equal CANVAS_HEIGHT.
The design and decomposition of this function is up to you, but we suggest the following milestones:
- Implement a function that-given the dimensions and coordinates of a bar, as well as the corresponding hasthag string-draws a bar and label at the top of the bar. We've provided some constants in the starter code that will be helpful here.
- Once you have the functionality to draw a single bar working, produce a dictionary of just the top ten hashtags for the user using the get_top_tags function. For each hashtag, plot a bar with the correct position and size.
CANVAS_WIDTH = 1000
CANVAS_HEIGHT = 600
TEXT_TOP_OFFSET = 10
TEXT_SIDE_OFFSET = 3
def draw_bar(canvas, tag_label, left_x, top_y, right_x, bottom_y):
"""
Given top-left and bottom-right coordinates for a bar and a label,
draw the bar
"""
canvas.create_rectangle(left_x, top_y, right_x, bottom_y, fill="light blue", outline="blue")
canvas.create_text(left_x + TEXT_SIDE_OFFSET, top_y+ TEXT_TOP_OFFSET, anchor='w',
font=('Calibri', '10'),
fill='Blue',
text=tag_label)
def visualize_trends(canvas, user_tags, user_name):
if user_name not in user_tags:
return
tags = get_top_tags(user_tags[username])
totalNum = sum(tags.values()) #grab the total number of hashtags they used
tags_list = list(tags.keys()) #to grab all the tags we need and store in a list
space = CANVAS_WIDTH/len(tags_list)
for i in range(len(tags_list)):
tag = tags_list[i]
count = tags[tag]
height = CANVAS_HEIGHT * (count/ totalNum)
x1 = space *i
y1 = CANVAS_HEIGHT - height
x2 = space*i + space
y2 = CANVAS_HEIGHT -1
tag_label = tag + " " + str(count)
draw_bar(canvas, tag_label, x1, y1, x2, y2)