As described in the handout, every value in Python is stored in memory, and variables refere to addresses in memory. For example:
n = 42
m = 43
print("The location of 42 in memory:", id(n))
print("The location of 43 in memory:", id(m))
The effect of code like n = m
is to make n
refere to the same location in memory as m
(which means that both n
and m
would be referring to the address where 43 is stored)
n = m
print("n now refers to:", id(n), "and the value stored at that address is:", n)
print("m now refers to:", id(m), "and the value stored at that address is:", m)
Exactly the same kind of thing happens with lists, if we are careful to do exactly the same thing:
L = [42, 43]
M = [45, 46]
print("The location of L in memory:", id(L))
print("The location of M in memory:", id(M))
M = L
print("M now refers to:", id(M), "and the value stored at that address is:", M)
print("L now refers to:", id(L), "and the value stored at that address is:", L)
Note, however, that every time we have notation like [42, 43]
, it means that we're creating a new list:
L1 = [3, 4]
print("id(L1)=", id(L1))
L2 = [3, 4]
print("id(L2)=", id(L2))
It so happens that L1
and L2
have the same values, but they are stored at different addresses.
Back to L
and M
. Remember that we set them to refer to the same address. That makes the aliases. If I modify the contents of one, that modifies the contents stored at address id(L)
, but that means it also modifies the conents stored at address id(M)
(which is the same address)
M[0] = 40
print("M = ", M)
print("L = ", L)
How do we avoid this? We actually already saw the answer above:
L = [M[0], M[1]]
L[0] = 50
print("L = ", L)
print("M = ", M)
[M[0], M[1]]
created a new list, put it in a new location in memory, and then made L
refere to it. Initially, since L
and M
both have two elements, they had the same contents. But modifying the contents of L
didn't change M
.
We actually already saw how to accomplish this in general (without knowing in advance how many elements there are in M
):
L = M[:] #shorthand for L = [M[0], M[1], ...., M[len(M)-1]]
In general, slicing is shorthand for creating a new list. For example, M[3:7]
is just shorthand for [M[3], M[4], ..., M[6]]
We can get away with not doing slicing at all. The following will make sure that L
and M
are stored at different addresses, and have the same contents
L = [] #create a new empty list, and make L refer to it
for e in M:
L.append(e)
As discussed before, local variables only exist for the duration of the run of the function. If a local variable has the same name as a global variable, it "masks" it while we're in the function.
def correct_gpa(gpa):
print("Initially, local gpa:", gpa)
average = 99.5
gpa = 3.99
print("Local gpa:", gpa)
gpa = 3.98
correct_gpa(gpa)
print("Global gpa is still", gpa)
#print(average) #would cause an error, average is a local variable that doesn't exist when we're not running correct_gpa
Here, gpa
is also the parameter. A parameter is a special kind of local variable. The first thing that happens when you call a function correct_gpa is <local gpa> = 3.98
. That is, the local gpa gets initialized to whatever is the value that we're calling correct_gpa
with
This matters when we are passing lists to functions:
def inflate_gpa_list(gpa_list):
gpa_list[0] = gpa_list[0]*1.1
gpa_list[-1] = gpa_list[-1]*1.1
L = [3.6, 3.7, 3.9]
inflate_gpa_list(L)
print("L is now", L)
What happenned? The first thing to happen when inflate_gpa_list
was called is gpa_list = L
. That means that gpa_list
and L
were referring to the same address. Modifying the contents of one also modified the contentsn of the other!
The issue didn't come up with integers, because we can't modify the contents of integers. The following is an example where we're not modifying the contents of lists, but rather reassigning new lists to variables that already referred to lists. The bahviour is exactly as with integers
def change_gpa_list(gpa_list):
gpa_list = [4,4,4]
L = [3, 3, 3]
change_gpa_list(L)
print("L is still:", L)
So we saw how to change the contents of a list. Suppose you have an integer, and you want it to refer to something new, and that something new is computed using a function. How to do that? Using the assignment (=
) operator:
def double_num(n):
return n * 2
n = 5
n = double_num(n)
print("n is now", n, "(of course)")
The same strategy works with lists, if you don't mind creating new lists and placing them in memory. (In many cases, there is no reason that you would mind doing that.)