Mutability, Aliasing, and Cloning in Python.
Mutability:
In Python, the concept of mutability refers to whether or not an object’s state (its data or content) can be changed after it is created without changing its location in memory. Objects in Python are classified as either mutable or immutable.
Mutable vs Immutable Objects:
Mutable Objects:
Mutable objects are those whose state or content can be modified after creation on the same memory location. Examples of mutable objects include:
- Lists: You can add, remove, or change elements.
- Dictionaries: You can add, remove, or change key-value pairs.
- Sets: You can add or remove elements.
Example code —
L = [1,2,3]
print(id(L)) #139652074757168
L.append(4)
print(L)
print(id(L)) #139652074757168
Id before and after appending 4 is the same, as list is a mutable data type means list gives the power to change the value on the same memory location.
Immutable Objects:
Immutable objects are those whose state or content cannot be changed after creation. Examples of immutable objects include:
- Strings: You cannot change the characters in a string.
- Tuples: You cannot change the elements in a tuple.
- Numbers: Integers, floats, etc., cannot be changed.
Example code —
# String (immutable)
my_string = "hello"
# my_string[0] = 'H' # This will raise an error because strings are immutable
# Tuple (immutable)
my_tuple = (1, 2, 3)
# my_tuple[0] = 4 # This will raise an error because tuples are immutable
Also if you try to change the value like this —
T = (1,2,3)
print(id(T)) #139652076050304
T = T + (4,)
print(T)
print(id(T)) #139652075362160
In this code when you try to add a new tuple to the existing one then the new tuple formed is stored in some other location instead of the original location of tuple T.
Why mutability is dangerous in certain scenarios?
Mutability can be dangerous in scenarios when you use aliasing.
Aliasing:
Aliasing happens when multiple variables refer to the same mutable object. Changes made to the object through one alias are reflected in all other aliases, which can lead to unexpected behavior and bugs.
Example code 1:
# Create a list and assign it to a variable
original_list = [1, 2, 3]
# Create an alias by assigning the list to another variable
alias_list = original_list
# Modify through alias
alias_list.append(4)
print(original_list) # [1, 2, 3, 4]
print(alias_list) # [1, 2, 3, 4]
Example code 2:
def func(data):
data.append(4)
a = [1,2,3]
func(a)
print(a) #[1,2,3,4]
Example code 2, is a more dangerous scenario, as here you are able to change the global variable without using the global keyword inside the program which is so risky.
When working with mutable objects, unintended side effects can occur if you are not aware that multiple variables are referencing the same object. This can lead to bugs that are difficult to trace. To solve the issue of Aliasing, we can use cloning or deep copy.
Cloning:
When we need to copy a mutable object, but also don’t want to have the problem of aliasing, then we can use cloning. If we make a clone of an object, then it is stored in some other memory location instead memory location of the original one, thus when we make any changes in the original or copied object, it will not affect each other. Two ways of cloning are —
Syntax 1:
a = [1,2,3]
# cloning
b = a[:]
print(id(a)) #139652074408688
print(id(b)) #139652074409728
b.append(4)
print(b) #[1,2,3,4]
print(a) #[1,2,3]
Syntax 2:
a = [1,2,3]
# cloning
b = a.copy()
print(id(a)) #139652074408688
print(id(b)) #139652074409728
b.append(4)
print(b) #[1,2,3,4]
print(a) #[1,2,3]
Why Some data types are Mutable and some are Immutable?
- Mutable objects are useful when you need to change or update the data frequently. However, they can lead to unintended side effects if not managed carefully, especially when passed to functions.
- Immutable objects provide safety and predictability since their state cannot be altered. These are helpful when you need to perform read-only operations and do not want to change anything once written.