Python - mutability, immutability
a = 10
print(hex(id(a)), hex(id(10)))
a = 20
print(hex(id(a)), hex(id(20)))
You see from above, when you reassign a value, Python first creates that object in memory, then assigns a reference to that object in memory to the variable. The original address was not updated, instead a new address was assigned. This is because integers are immutable.
a = [1,2,3]
print(hex(id(a)))
a.append(4)
print(hex(id(a)))
print(a)
Here, the address remains the same, while the content was updated. This is mutation as lists are mutables. A new address space was not assigned. Instead, the contents in the original was updated.
a.append(10)
for e in a:
print(f'{e} @ {hex(id(e))}')
Each element in the list is a different object at a different address. When we added 10
, the same address as before was assigned (note from earlier cell).
c = 20
d = 20
print(id(c) == id(d))
print(hex(id(c)), hex(id(d)))
print(c == d)
print(c is d)
Above, Python used the same address for both the objects, which is equivalent to writing c = d = 20
. This is safe since integers are immutable.
e1 = f1 = [20,30,40] # same address
e2 = ['a', 'b'] # different address
f2 = ['a', 'b']
print(id(e1) == id(f1))
print(hex(id(e1)), hex(id(f1)))
print(id(e2) == id(f2))
print(hex(id(e2)), hex(id(f2)))
In the code above, when you use var = var2 = value
, then the same address is assigned to both variables. However, in the next pattern, even though the values are identical, Python assigned different addresses since lists are mutable and it makes sense to instantiate them separately in memory.
print(hex(id(e2[0])), hex(id(f2[0]))) # should be the same:
Even thought Python assigned two different lists in memory, the contents of the lists still point to the same string objects in memory as strings are immutable.
l1 = [1,2,3]
l2 = ['a','b','c']
t1 = (l1, l2)
print(hex(id(t1))) # print address
t1[0].append(4) # is this a mutation? Not actually
print(hex(id(t1))) # print address again and compare
The tuple is immutable, however, its elements can change because, the tuple’s reference to the list remains the same. The list’s references got changed, but that does not break the holding tuple’s immutability!
l1 = [1,2,3]
print(hex(id(l1)))
l1.append(4)
print(hex(id(l1))) # should be same as above
l1 = l1 + [5]
print(l1)
print(hex(id(l1))) # will be different than from an append operation
What happened here? When you run l1 = l1 + [val]
, it evaluates the expression and assigns a new memory for the result, eventhough you ask it to update the same object.
def modify_str(s):
print(f'Incoming s @ : {hex(id(s))}')
s = s + " world"
print(f'Post OP s @ : {hex(id(s))}')
s = 'hello'
print(hex(id(s)))
modify_str(s)
print(s)
print(hex(id(s)))
The address of s
inside the func is initially the same. But when an operation was ran, since strings are immutable, a new object was created. However, outside the scope of the function, the value of s
remains the same and at the same address.
def modify_lst(l):
print(f'Incoming list @ : {hex(id(l))}')
l.append(200)
print(f'PostOp list @ : {hex(id(l))}')
l1 = [1,2,3]
print(f'Outside list @ : {hex(id(l1))}')
modify_lst(l1)
print(l1)
print(f'Outside list @ : {hex(id(l1))}')
Thus, functions can modify the value of variables outside their scope as well, since Python is pass by reference.