Solving the Problem Sets of CS50's Introduction to Programming with Python β One at a Time: Final Project and Beyond
Eda
Posted on June 8, 2022
Read the original blog post here.
The final week has arrived, and today we do not have any problem sets. Before anything, give yourself a pat on the back for coming this far!
It has been quite a delightful journey. We have started with the basic blocks of programming, variables and functions, dealt with flow control using conditionals, iterated with loops, handled exceptions, scratched the surface of the world of Python libraries, written tests for our programs to make sure that they work as we intend them to do, worked with files, learned to love regular expressions, and last week, peeked into the realm of object-oriented programming. Today in et cetera we will be looking at some other tools in our toolkit. Phew! If you have started this journey from absolute zero, you have indeed come far! Even if you already have some knowledge before starting the course, congratulations to you as well! It is not easy to dedicate oneself through all these weeks. You can always find the posts on previous problem sets in the archive as well.
The theme of this series, as well as the course, has been one important point: when in doubt, read the documentation. Even though the official Python documentation might not seem as friendly at first, you have been using it for many weeks and must be familiar with it already. There are lots more to discover, of course, and it is our number-one friend. Some of these things to discover are already shown in the lecture, so, let's remember them with very simple examples. (And, get ready for a bunch of Harry Potter references.)
Sets
A set is, at the very basic level, a data structure that has no duplicates. So, let's say you want to look at the distinct broomsticks that Harry Potter used for Quidditch. Easy to do it with a set:
broomsticks = [
'Nimbus 2000',
'Nimbus 2000',
'Firebolt',
'Firebolt',
'Firebolt',
'Firebolt',
]
print(set(broomsticks)) # {'Firebolt', 'Nimbus 2000'}
Globals
Global variables are usually frowned upon; especially, using the global
keyword is something you must avoid unless you are absolutely sure what you are doing. You can think of a global variables as simply variables outside a function. They cannot just be changed right away inside a function, but are read only in that sense. To change the value of a global variable inside a function, you use the global
keyword. Let's say we are completing the title of our favorite book in the Harry Potter series:
half_title = 'Chamber of Secrets'
def change_half_title():
half_title = 'Goblet of Fire'
change_half_title()
print(f'Harry Potter and the {half_title}')
# -> Harry Potter and the Chamber of Secrets
Of course, it did not change as we expected. However, with the global
keyword, it works:
half_title = 'Chamber of Secrets'
def change_half_title():
global half_title
half_title = 'Goblet of Fire'
change_half_title()
print(f'Harry Potter and the {half_title}')
# -> Harry Potter and the Goblet of Fire
Again, it is not very nice to look at, so avoid this kind of implementation as much as you can.
Constants
If you have seen the lecture, you already know that Python do not have constant types. A "constant" variable, though, is indicated with capital letters:
SCHOOL_NAME = 'Hogwarts School of Witchcraft and Wizardry'
def invite_student():
return f'We are pleased to inform you that you have been accepted at {SCHOOL_NAME}.'
print(invite_student())
# -> We are pleased to inform you that you have been accepted at Hogwarts School of Witchcraft and Wizardry.
Type Hints
Python is a dynamically-typed language, however, we can still use type hints to make sure we avoid TypeError
s.
For example, as you can find the similar example in the documentation for typing, we can indicate the expected types for arguments and return values of a function:
def greeting(name: str) -> str:
return f'Hello, {name}!'
Also, as mentioned in the lecture, mypy
is a popular library that you can use for type hinting.
Docstrings
Docstrings can occur in a module, a function, or a class. The simplest one-line docstring looks like this:
def add(n, n1):
"""Add two numbers."""
return n + n1
The conventions on how to use docstrings can be found here in this PEP.
argparse
argparse
is a module that comes built-in with Python, literally a "parser for command-line options, arguments and sub-commands".
There is a great tutorial on the official documentation already, so, we are not going to dive deep into it here. The simplest thing you can do might look like this. Say, we have a file called spell.py
, and we want to pass in the argument -s
to our program to indicate the type of spell we want to create. We want the proper incantation printed on our terminal. Let's see:
# π spell.py
import argparse
incantations = {
'patronus': 'Expecto Patronum!',
'summon': 'Accio!',
'unlock': 'Alohomora!',
'explode': 'Bombarda!',
'levitate': 'Wingardium Leviosa!',
'stun': 'Stupefy!'
}
parser = argparse.ArgumentParser()
parser.add_argument('-s')
args = parser.parse_args()
print(incantations[args.s])
We can see it with the right command:
$ python spell.py -s unlock
Alohomora!
*args, **kwargs
We have mentioned the unpacking operators briefly in a previous post on problem set 4. The example looked like this:
values = [0, 5, 2]
print(*values) # 0 5 2
# Prints 0, 2, 4 respectively
for i in range(*values):
print(i)
houses = {
'Gryffindor': 'courage',
'Ravenclaw': 'intelligence',
'Hufflepuff': 'loyalty',
'Slytherin': 'ambition'
}
people = {
'Harry Potter': 'Gryffindor',
'Hermione Granger': 'Gryffindor',
'Luna Lovegood': 'Ravenclaw'
}
print({**houses, **people}) # {'Gryffindor': 'courage', 'Ravenclaw': 'intelligence', 'Hufflepuff': 'loyalty', 'Slytherin': 'ambition', 'Harry Potter': 'Gryffindor', 'Hermione Granger': 'Gryffindor', 'Luna Lovegood': 'Ravenclaw'}
They are super handy for many kinds of problems you encounter, so, another great tool in our toolkits.
map
With the map
function, we can map a function to each item of an iterable. Creating a list of the squares of each number in a "numbers" list might look like this:
numbers = [3, 5, 7, 11, 13]
squared = list(map(lambda n: n**2, numbers))
print(squared) # [9, 25, 49, 121, 169]
Notice that we also convert the return value of map
to a list
, as the map
function returns a Map
object.
List comprehensions
If you have been following the series, you already know about the list comprehensions way back in Problem Set 2. It is a Pythonic way to append to a list, so instead of doing something like this:
word = 'CS50'
digits_in_word = []
for char in word:
if char.isdigit():
digits_in_word.append(char)
print(digits_in_word) # ['5', '0']
Just write a one-liner that achieves the same result:
word = 'CS50'
digits_in_word = [char for char in word if char.isdigit()]
print(digits_in_word) # ['5', '0']
filter
We can also filter an iterable, returning only the values we are interested in.
The same example above in list comprehensions can also be solved like this:
word = 'CS50'
digits_in_word = list(filter(str.isdigit, word))
print(digits_in_word) # ['5', '0']
Also, just like in map
, notice we also convert the return value to a list
. We also do not call the str.isdigit
inside filter
, we only pass a reference to that function.
Dictionary comprehensions
Similar to list comprehensions, dictionary comprehensions are also another βsometimes elegant, sometimes notβ way to create dictionaries. To implement a very simple one, let's initialize all the Hogwarts house points to 0 for the start of the term:
houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']
house_points = {house: 0 for house in houses}
print(house_points) # {'Gryffindor': 0, 'Hufflepuff': 0, 'Ravenclaw': 0, 'Slytherin': 0}
It works as intended, and initializes all the house points 0.
enumerate
Here is a Pythonic way to iterate over an iterable. Similar to the lecture example, let's say that this time we want to print the names of the houses, also indicated with the first value of '1', instead of '0'. We do not have to write something like this:
houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']
for i in range(len(houses)):
print(i + 1, houses[i])
# ->
# 1 Gryffindor
# 2 Hufflepuff
# 3 Ravenclaw
# 4 Slytherin
There is a more elegant way to do it:
houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']
for index, house in enumerate(houses, start=1):
print(index, house)
# ->
# 1 Gryffindor
# 2 Hufflepuff
# 3 Ravenclaw
# 4 Slytherin
Notice that the enumerate
function also takes a start
argument to start from the number that is passed.
Generators
Finally, also mentioned in the lecture, a generator function is a "function that returns a generator iterator". With a generator function that yields as opposed returns a value, we can save memory with lazy evaluation. The Python documentation also has a tutorial on generators, and similar to the example in the lecture, the very simplest implementation might look like this:
def main():
for _ in gen(1000000):
print(_)
def gen(n):
for _ in range(n):
yield _
if __name__ == '__main__':
main()
These are all the topics we have explored this week. From now on, we are left with our very own Final Project to implement. For this, you are free to create anything that excites you, any kind of problem that you want to solve β of course, following the given specifications for the project. And, after that, you might think that is all, and that we are finished, but, are we?
Conclusion
Now, looking back, we have gathered many useful tools in our toolkit to do whatever we want to do. But, should we do whatever we want to do just because we can?
You probably have ideas for the answer to that question. It is easy to get excited about all kinds of things you can create once you know how to do them. But, once you start to create things, always remember that using and trusting technology as a solution to all problems is not always the case. Now that you have the power and knowledge to do so, remember that it is absolutely vital to create software that respects users' freedom, that is open and trustworthy. Remember that privacy is a human right, even if there might have already been much talk about it β yet, usually without honesty. Do not underestimate your current level of knowledge, you have tremendous power in your hands with the tools you can use. And, yes, one more thing to remember β code is speech.
These all might sound like out of context, why should you even bother to think about them? After all, assuming you only have taken this introductory course, and are still at the beginning of your programming journey, and have a long way to go. But, hopefully, you undoubtedly agree that we need good things in life β good software that respects human dignity and helps the progress of humanity is one of them. If you think these are some grand ideologies for a "beginner" like you βwhich, honestly, I also consider myself a "beginner" in many things at this pointβ, remember that each piece of knowledge will eventually add up to another, so, even if you are not going to pursue a programming path in your life at all; that is fine, because at least this will be how you look at things, have a stronger sense of self-agency, and a more educated opinion in the decisions that affects us all.
With that in mind, that is the end of the series!
If you have read so far, thank you. And, as always, happy coding. π
Posted on June 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.