More mutation, dictionaries
Testing mutating functions
How would we test the last function we defined in our previous lecture?
def test_complete_by_description(): item1 = TodoItem(date(2020, 11, 25), "desc1", ["tag"], False) item2 = TodoItem(date(2020, 11, 25), "desc2", ["tag"], False) todo = [item1, item2] complete_by_description(todo, "desc1") test("completed in list", todo, [TodoItem(date(2020, 11, 25), "desc1", ["tag"], True), TodoItem(date(2020, 11, 25), "desc2", ["tag"], False)]) test("correct item completed", item1.done, True)
There are a couple of things to notice here:
- Like our test for
remove_finished
, we structure our test by defining a list, calling the function, and then testing to see that the list has changed. This is the pattern we’ll use to test functions that mutate data. - One call to
test
refers to thetodo
list; the other refers toitem1
. Both work–the function modifies the todo item we have added to the list.
Modifying lists versus assigning to variables
Last time, we contrasted these two functions on todo lists:
def find_unfinished_items(todo_list: list) -> list: return list(filter(lambda item: not item.done, todo_list)) def remove_finished(todo_list: list): """remove all of the completed items from the TODO list""" completed_items = list(filter(lambda item: item.done, todo_list)) for item in completed_items: todo_list.remove(item)
Both of these functions “remove” finished items from a list. These functions,
however, are really doing fundamentally different
things. find_unfinished_items
returns a new list, containing every
unfinished item from its input. remove_finished
modifies its input, removing
the finished items.
Here’s another potential version of remove_finished
:
def remove_finished(todo_list: list): """remove all of the completed items from the TODO list""" todo_list = list(filter(lambda item: not item.done, todo_list))
If we run this function:
>>> remove_finished(todo_list) >>> todo_list
We will see that our list is not modified. What’s going on here? Assigning to
the name todo_list
within our function doesn’t affect the list we pass in; in
order to modify that list, we need to call list methods. In a couple of lectures
we’ll learn (much) more about how this works; for now, just remember that if you
want to write a function that modifies a data structure you can’t just assign
your function’s input to a new structure.
Dictionaries
What if we wanted to have multiple users, each of whom has their own TODO list?
# in todo2.py from dataclasses import dataclass from datetime import date from testlight import * @dataclass class TodoItem: deadline: date tags: list description: str done: bool # doug's TODO list class_item = TodoItem(date(2020, 11, 11), ["school", "class"], "Prepare for CSCI 0111", False) avocado_item = TodoItem(date(2020, 11, 16), ["home", "consumption"], "Eat avocado", False) birthday_item = TodoItem(date(2020, 11, 20), ["home", "friends"], "Buy present for friend", False) # kathi's TODO list tas_item = TodoItem(date(2020, 11, 20), ["school", "class"], "Hire TAs for CS18 ", False)
How could we change our design to accomodate this?
Adding a user field
We could add a user
field to our TODO items:
@dataclass class TodoItem: user: str deadline: date tags: list description: str done: bool ## doug's TODO list class_item = TodoItem("doug", date(2020, 11, 11), ["school", "class"], "Prepare for CSCI 0111", False) avocado_item = TodoItem("doug", date(2020, 11, 16), ["home", "consumption"], "Eat avocado", False) birthday_item = TodoItem("doug", date(2020, 11, 20), ["home", "friends"], "Buy present for friend", False) ## kathi's TODO list tas_item = TodoItem("kathi", date(2020, 11, 20), ["school", "class"], "Hire TAs for CS18 ", False) todos = [class_item, avocado_item, birthday_item, tas_item]
We still track one big list of TODO items, but we can filter it to have only the TODO items for a given user.
This isn’t a great structure–we’re rarely if ever going to want to look at multiple users’ TODO items at the same time, but right now they are all mixed together.
Another dataclass
We could also do something like this:
@dataclass class TodoItem: deadline: date tags: list description: str done: bool @dataclass class TodoList: user: str todos: list # of TodoItems ## doug's TODO list class_item = TodoItem(date(2020, 11, 11), ["school", "class"], "Prepare for CSCI 0111", False) avocado_item = TodoItem(date(2020, 11, 16), ["home", "consumption"], "Eat avocado", False) birthday_item = TodoItem(date(2020, 11, 20), ["home", "friends"], "Buy present for friend", False) ## kathi's TODO list tas_item = TodoItem(date(2020, 11, 20), ["school", "class"], "Hire TAs for CS18 ", False) todos = [TodoList("doug", [class_item, avocado_item, birthday_item]), TodoList("kathi", [tas_item])]
We could have a list of TodoList
instances, each of which has its own list of
TODO items.
This is a bit better than the previous option–now all of the TODO items for a given user are in the same place. But–how should we get a user’s TODOs from our big list? We’d probably use a filter or something similar.
Dictionaries to the rescue
There’s a better way to do this! We can use dictionaries:
@dataclass class TodoItem: deadline: date tags: list description: str done: bool ## doug's TODO list class_item = TodoItem(date(2020, 11, 11), ["school", "class"], "Prepare for CSCI 0111", False) avocado_item = TodoItem(date(2020, 11, 16), ["home", "consumption"], "Eat avocado", False) birthday_item = TodoItem(date(2020, 11, 20), ["home", "friends"], "Buy present for friend", False) ## kathi's TODO list tas_item = TodoItem(date(2020, 11, 20), ["school", "class"], "Hire TAs for CS18 ", False) todos = {"doug": [class_item, avocado_item, birthday_item], "kathi": [tas_item]}
Dictionaries associate keys with values. They are used when we want to track
mappings between values–in this example, between users and lists of TODO
items. "doug"
and "kathi"
are the keys; each is associated with a different
TODO list.
We can access dictionaries as follows:
>>> "doug" in todo_list True >>> "monica" in todo_list False >>> todo_list["doug"] [...] >>> todo_list["kathi"] [...] >>> todo_list["monica"] ERROR
We can also write a loop over all of the elements of a dictionary:
def most_todos(todos: dict) -> str: most = 0 user_most = "" for user in todos: if len(todos[user]) > most: most = len(todos[user]) user_most = user return user
Notice what’s happening here: our loop iterates over all of the dictionary’s
keys. We get the values by evaluating todos[user]
.
We can modify a dictionary:
>>> todo_list["monica"] = [] >>> todo_list["monica"] [] >>> todo_list["doug"] = [] # overwrites previous value! >>> todo_list["doug"] []