set#
A set is a collection of unique elements. It is an unordered collection of items. Sets are used to store multiple items in a single variable. It is a collection data type in Python.
Example without using set
#
students = [
{"name": "Hermione", "house": "Gryffindor"},
{"name": "Harry", "house": "Gryffindor"},
{"name": "Ron", "house": "Gryffindor"},
{"name": "Draco", "house": "Slytherin"},
{"name": "Padma", "house": "Ravenclaw"},
]
houses = []
for student in students:
if student["house"] not in houses:
houses.append(student["house"])
for house in sorted(houses):
print(house)
Example without re-inveting the wheel: using set
#
students = [
{"name": "Hermione", "house": "Gryffindor"},
{"name": "Harry", "house": "Gryffindor"},
{"name": "Ron", "house": "Gryffindor"},
{"name": "Draco", "house": "Slytherin"},
{"name": "Padma", "house": "Ravenclaw"},
]
houses = set()
for student in students:
houses.add(student["house"])
for house in sorted(houses):
print(house)
global#
The global keyword is used to declare that a variable inside the function is global (outside the function). If you want to change a global variable inside a function, you can use the global keyword to declare which variables are global.
Example where you use a variable that lives outside the function#
balance = 0
def main():
print("Balance:", balance)
if __name__ == "__main__":
main()
Example of code that tries to change a global variable inside a function#
balance = 0
def main():
print("Balance:", balance)
deposit(100)
withdraw(50)
print("Balance:", balance)
def deposit(n):
balance += n
def withdraw(n):
balance -= n
if __name__ == "__main__":
main()
The result is a UnboundLocalError
because the variable balance
is not defined inside the function deposit
.
Apparently is okey to read a global variable inside a function, but if you want to change it, you need to use the global
keyword.
Example of code that changes a global variable inside a function#
balance = 0
def main():
print("Balance:", balance)
deposit(100)
withdraw(50)
print("Balance:", balance)
def deposit(n):
global balance
balance += n
def withdraw(n):
global balance
balance -= n
if __name__ == "__main__":
main()
Example of code with OOP#
class Account:
def __init__(self):
self._balance = 0
@property
def balance(self):
return self._balance
def deposit(self, n):
self._balance += n
def withdraw(self, n):
self._balance -= n
def main():
account = Account()
print("Balance:", account.balance)
account.deposit(100)
account.withdraw(50)
print("Balance:", account.balance)
if __name__ == "__main__":
main()
Usually using global variables is not a good practice. It is better to use classes and objects.
Note for myself: the instance variable balance
and all the other instance variables by definition can be accessed by all the methods of the class.
constants#
A constant is a type of variable whose value cannot be changed.
Example of code with “constants”#
MEOWS = 3
for _ in range(MEOWS):
print("meow")
There’s actually no way to define a constant in Python. The convention is to use uppercase letters for the variable name to indicate that it should be treated as a constant.
Constants in OOP#
class Cat:
MEOWS = 3
def meow(self):
for _ in range(Cat.MEOWS): # or self.MEOWS
print("meow")
cat = Cat()
cat.meow()
Type Hints, mypy#
mypy
is a static type checker for Python. It is a tool that can be used to check the types of variables and functions in your code.
Example of code where an error is detected by mypy
#
def meow(n):
for _ in range(n):
print("meow")
number = input("Number: ")
meow(number)
MyPy will detect the error because the function input
returns a string, and the function meow
expects an integer.
Example of code with type hints#
def meow(n: int):
for _ in range(n):
print("meow")
number: int = input("Number: ")
meow(number)
The code is still wrong, but now mypy
can detect the error much quicker by adding type hints.
Correct version of the code#
def meow(n: int):
for _ in range(n):
print("meow")
number: int = int(input("Number: "))
meow(number)
Return type hints: error version#
def meow(n: int) -> None:
for _ in range(n):
print("meow")
number: int = int(input("Number: "))
meows: str = meow(number)
print(meows)
Return type hints: correct version#
def meow(n: int) -> str:
return "meow\n" * n
number: int = int(input("Number: "))
meows: str = meow(number)
print(meows, end="")
Docstrings#
A docstring is a string that is used to document a function, method, or class. It is used to describe what the function does, what parameters it takes, and what it returns.
There’s a standarized way on how to document your functions, methods, and classes in Python, the PEP (Python Enhancement Proposal) is the PEP 257
Example of code with a docstring#
def meow(n: int) -> str:
"""
Meow n times.
:param n: Number of times to meow.
:type n: int
:raise TypeError: If n is not an int.
:return: A string of n meows, one per line.
:rtype: str
"""
return "meow\n" * n
number: int = int(input("Number: "))
meows: str = meow(number)
print(meows, end="")
argparse#
The argparse
module is used to parse command-line arguments in Python. It is a standard module that comes with Python.
Example of code with argparse
#
import argparse
parser = argparse.ArgumentParser(description="Meow like a cat.")
parser.add_argument("-n", default=1, help="number of times to meow", type=int)
args = parser.parse_args()
for _ in range(args.n):
print("meow")
Note: if you specify that the argument is a type int
, you don’t need to convert it to an integer.
unpacking#
Unpacking is the process of extracting values from a collection, such as a list or a tuple, and assigning them to variables.
A very simple example of unpacking#
first, _ = input("What's your name? ").split(" ") # Here is the unpacking
print(f"hello, {first}")
We used the foo, bar = "foo bar".split(" ")
to unpack the values of the list returned by the split method.
Unpacking a list#
def total(galleons, sickles, knuts):
return (galleons * 17 + sickles) * 29 + knuts
coins = [100, 50, 25]
print(total(*coins), "Knuts") # Here is the unpacking
We used the *
operator to unpack the values of the list coins
and pass them as arguments to the function total
.
Unpacking a dictionary#
def total(galleons, sickles, knuts):
return (galleons * 17 + sickles) * 29 + knuts
coins = {"galleons": 100, "sickles": 50, "knuts": 25}
print(total(**coins), "Knuts") # Here is the unpacking
We used the **
operator to unpack the values of the dictionary coins
and pass them as arguments to the function total
.
*args
and **kwargs
#
*args
is used to pass a variable number of arguments to a function.**kwargs
is used to pass a variable number of keyword arguments to a function.
def f_positional(*args, **kwargs):
print("Positional:", args)
# The output of this function is: Positional: (100, 50, 25)
f_positional(100, 50, 25)
def f_keyword(*args, **kwargs):
print("Keyword:", kwargs)
# The output of this function is: Keyword: {'galleons': 100, 'sickles': 50, 'knuts': 25}
f_keyword(galleons=100, sickles=50, knuts=25)
map#
The map
function is used to apply a function to each item in a list or other iterable.
map(function, iterable)
Example of code without using map
#
def main():
yell("This", "is", "CS50")
def yell(*words):
uppercased = []
for word in words:
uppercased.append(word.upper())
print(*uppercased)
if __name__ == "__main__":
main()
Example of code using map
#
def main():
yell("This", "is", "CS50")
def yell(*words):
"""
We use str.upper instead of something.upper()
because instead of actually applying the method
to the string, we are just passing the method
itself to the map function.
"""
uppercased = map(str.upper, words)
print(*uppercased)
if __name__ == "__main__":
main()
List Comprehensions#
List comprehensions are a concise way to create lists in Python. They are used to create a new list by applying an expression to each item in an existing list.
Example of code using a list comprehension instead of a for
loop or map
#
def main():
yell("This", "is", "CS50")
def yell(*words):
uppercased = [word.upper() for word in words]
print(*uppercased)
if __name__ == "__main__":
main()
filter#
The filter
function is used to filter items from a list or other iterable.
filter(function, iterable)
Example of code without using filter
#
students = [
{"name": "Hermione", "house": "Gryffindor"},
{"name": "Harry", "house": "Gryffindor"},
{"name": "Ron", "house": "Gryffindor"},
{"name": "Draco", "house": "Slytherin"},
{"name": "Padma", "house": "Ravenclaw"},
]
gryffindors = [
student["name"] for student in students if student["house"] == "Gryffindor"
]
for gryffindor in sorted(gryffindors):
print(gryffindor)
In the code above, we used a list comprehension with a conditional to filter the students that belong to the house Gryffindor.
Example of code using filter
#
students = [
{"name": "Hermione", "house": "Gryffindor"},
{"name": "Harry", "house": "Gryffindor"},
{"name": "Ron", "house": "Gryffindor"},
{"name": "Draco", "house": "Slytherin"},
{"name": "Padma", "house": "Ravenclaw"},
]
def is_gryffindor(s):
return s["house"] == "Gryffindor"
gryffindors = filter(is_gryffindor, students)
for gryffindor in sorted(gryffindors, key=lambda s: s["name"]):
print(gryffindor["name"])
Dictionary Comprehensions#
Dictionary comprehensions are a concise way to create dictionaries in Python. They are used to create a new dictionary by applying an expression to each item in an existing dictionary.
Example of code not using a dictionary comprehension#
students = ["Hermonie", "Harry", "Ron"]
gryffindors = []
for student in students:
gryffindors.append({"name": student, "house": "Gryffindor"})
print(gryffindors)
Example of code using a dictionary comprehension#
students = ["Hermonie", "Harry", "Ron"]
gryffindors = {student: "Gryffindor" for student in students}
print(gryffindors)
enumerate#
The enumerate
function is used to add a counter to an iterable.
enumerate(iterable, start=0)
Example of code without using enumerate
#
students = ["Hermonie", "Harry", "Ron"]
for i in range(len(students)):
print(i + 1, students[i])
Example of code using enumerate
#
students = ["Hermonie", "Harry", "Ron"]
for i, student in enumerate(students, start=1):
print(i + 1, student)
Generators, Iterators, yield#
A generator is a function that returns an iterator. It generates values using the yield
keyword.
Example of code that not works due the lack of using a generator#
def main():
n = int(input("What's n? "))
for i in range(n):
print(sheep(i))
def sheep(n):
flock = []
for i in range(n):
flock.append("🐑" * i)
return flock
if __name__ == "__main__":
main()
The problem in the code above is that the function sheep
first generates all the values and then returns them all at once. This is not efficient because it uses a lot of memory.
Example of code using a generator#
def main():
n = int(input("What's n? "))
for i in range(n):
print(sheep(i))
def sheep(n):
for i in range(n):
yield "🐑" * i
if __name__ == "__main__":
main()
The word yield
is used to return a value at the time, that way we don’t need to store all the values in memory but just one at a time.
yeild
is returning an iterator, so we can use it in a for
loop.