12.2 Mutable Lists

12.2 Mutable Lists

Let’s expand our study of updates yet again, this time looking at updating lists. We’ll start with lists.

Imagine that Shaunae wants to use a program to maintain her shopping list. She creates an initial list with two items:

shaunae_list = ["bread", "coffee"]

Do Now!

Shaunae wants to add eggs to her list. Write a line of code to accomplish this.

There are two ways you could have done this:

# approach 1
shaunae_list = shaunae_list + ["eggs"]

#approach 2
shaunae_list.append("eggs")

What is the difference between these two approaches? The difference lies in the impact on the heap.
  • The first version creates a new list containing "eggs", then puts the elements of the two lists together in a new list.

  • The second version inserts "eggs" into the existing list in the heap.

Let’s look at the directories for each version. Here’s the final directory for the first version:

Directory

  • shaunae_list

      1010

Heap

  • 1005: List(len:2)

  • 1006: "bread"

  • 1007: "coffee"

  • 1008: List(len:1)

  • 1009: "eggs"

  • 1010: List(len:3)

  • 1011: "bread"

  • 1012: "coffee"

  • 1013: "eggs"

The original version of shaunae_list is in address 1005, the list with "eggs" is in 1008, and the combined list is in 1010.

In contrast, the final directory for the second version would look like:

Directory

  • shaunae_list

      1010

Heap

  • 1005: List(len:3)

  • 1006: "bread"

  • 1007: "coffee"

  • 1008: "eggs"

Notice here that the length and contents of the original list are changed to include the newly-appended "eggs".

Do Now!

Which approach do you think is better? Why?

At first glance, the second approach might seem better because it doesn’t create additional unnecessary lists. Both approaches result in the same contents in shaunae_List, so there seems little benefit to using the additional space.

Unless, of course, we want to still have access to the old version of shaunae_list later on. The old list is still in the heap (though our current program has no name through which to access that old list). What if we instead had written the program this way?

shaunae_list = ["bread", "coffee"]
prev_list = shaunae_list
shaunae_list = ["paint", "brushes"] + shaunae_list

Now, if Shaunae realizes she goofed and put her art supply shopping on the grocery list on that last update, she could “undo” the update by resetting her list variable to the previous list:

shaunae_list = prev_list

Undoing a modification (just like the undo feature in document-editing tools) is just one example of where it can help to hang on to older versions of data for a little while. The point here is not to give a sophisticated treatment of undoing computations, but more to motivate that there are situations in which creating a new list is preferable to updating the old one.

When might we want to update, rather than preserve, the existing list?

Remember our discussion of aliasing? We wanted two people, Elena and Jorge to share access to a common bank account. Might we ever want a shared shopping list? Sure, Shaunae and her roommate Jonella do share a shopping list, so that they can both add items while letting either one go to the store.

Do Now!

Set up a shared shopping list that is accessible through two names, shaunae_list and jonella_list. Then, add an item to the list via one of these names and check that the item appears under the other name.

You might have written something like the following:

shaunae_list = ["bread", "coffee"]
jonella_list = shaunae_list
jonella_list.append("eggs")

If you load this code at the prompt and look at both lists at the end, you’ll see they have the same values.

In contrast, had we written the code as follows, only one of them would see the new item:

>>> jonella_list = ["apples"] + jonella_list
>>> jonella_list
["apples", "bread", "coffee", "eggs"]
>>> shaunae_list
["bread", "coffee", "eggs"]

Do Now!

Draw the memory diagram for the above program.

Exercise: Creating Lists of Accounts

In [REFSEC], we wrote a function to create new accounts for the bank. That function returned each new account as it was created. That meant that every newly-created account had to be associated with a name in the directory (otherwise we would not be able to access it from the heap).

Maintaining either a list or a dictionary of all the created accounts makes much more sense. We’d need only a single name for the collection of accounts, but could still access individual accounts as needed. For example, we might want an all_accts list that looks something like the following:

all_accts = [Account(8623, 100),
             Account(8624, 300),
             Account(8625, 225),
             ...
             ]

Do Now!

Write a program that creates an empty all_accts list, then adds a new Account to it each time create_acct is called. You will need to modify create_acct in order to do this. Here is the existing code as a starting point.

next_id = 1

def create_acct(init_bal: float) -> Account:
  global next_id
  new_acct = Account(next_id, init_bal, [])
  next_id = next_id + 1
  return new_acct

Do Now!

Did you include a line like global all_accts in your code? Why or why not?

If you used append to update the all_accts list, then you would not need to include global all_accts. Recall that global is needed to tell Python to update a variable in the top-level directory rather than the local directory. If you use all_accts.append, however, you are modifying the heap instead of the directory. There is no need for global if your code is only modifying heap contents.