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")
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"
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"
"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
andjonella_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")
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 newAccount
to it each timecreate_acct
is called. You will need to modifycreate_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.