Mutability
What is mutation?¶
What is mutation? Is it just changing value? No. It is changing value, while retaining the same address in memory.
a = 10
print(hex(id(a)), hex(id(10)))
a = 20
print(hex(id(a)), hex(id(20)))
0x7fd7d8521a50 0x7fd7d8521a50 0x7fd7d8521b90 0x7fd7d8521b90
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)
0x7fd7dcd88cc0 0x7fd7dcd88cc0 [1, 2, 3, 4]
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))}')
1 @ 0x7fd7d8521930 2 @ 0x7fd7d8521950 3 @ 0x7fd7d8521970 4 @ 0x7fd7d8521990 10 @ 0x7fd7d8521a50
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).
Mutable and immutable data types¶
You can define your classes to be either mutable or immutable.
Extending the concept from above, what happens when you assign the same value to two variables?
c = 20
d = 20
print(id(c) == id(d))
print(hex(id(c)), hex(id(d)))
True 0x7fd7d8521b90 0x7fd7d8521b90
print(c == d)
print(c is d)
True True
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)))
True 0x7fd7dcd8e4c0 0x7fd7dcd8e4c0 False 0x7fd7dcd8a680 0x7fd7dcd8e980
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:
0x7fd7da0b0330 0x7fd7da0b0330
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.
Immutable collections containing mutable elements¶
What happens when a tuple's elements are lists, can you edit the elements? How is mutability considered then?
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
0x7fd7dcba8cc0 0x7fd7dcba8cc0
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!
Operations on mutable objects¶
Not all operations on mutable objects are mutable. See below:
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
0x7fd7dcd8b540 0x7fd7dcd8b540 [1, 2, 3, 4, 5] 0x7fd7dcd8b940
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.
Function arguments and mutability¶
What happens when a function changes or modifies one of its parameters' value? Will it change it outside the function as well since python is pass by reference?
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)))
0x7fd7dc7000b0 Incoming s @ : 0x7fd7dc7000b0 Post OP s @ : 0x7fd7dcd826f0 hello 0x7fd7dc7000b0
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))}')
Outside list @ : 0x7fd7de1a7f00 Incoming list @ : 0x7fd7de1a7f00 PostOp list @ : 0x7fd7de1a7f00 [1, 2, 3, 200] Outside list @ : 0x7fd7de1a7f00
Thus, functions can modify the value of variables outside their scope as well, since Python is pass by reference.