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:

  1. 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.
  2. One call to test refers to the todo list; the other refers to item1. 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"]
[]