Python3: Mutable, Immutable…
Everything is an object!
Python is a beautiful language: easy to pick up, easy to read, easy to maintain. But a part of the reason it’s so easy is that there’s SO MUCH going on under the hood. At first, it can be great to just go along and say “I don’t need to know what’s going on in there!” However, understanding the in’s and out’s of Python will help prevent you from making major bugs in your programs. Let’s start off with the glue that holds everything together, objects. Literally everything is an object. Your module is an object, a function is an object, an integer is an object. Objects are simply instances of classes. We can think of objects as individuals while classes are the groups to which they belong. We are all humans (class human), but I am a unique individual (instance of class human).
Identity and Type:
Before we dive in to the intricacies of objects in Python, let’s start off with some background on two builtin functions: id() and type(). These two functions can give us crucial information about a variable in question. The type function will return the data type of the object, which is most often the same as the class that the object belongs to. For example, if I have a list l, performing type(l) will return the class list:
The id function will return the variable identifier of the variable of your choosing. All variables in Python refer to an object and the variable identifier is an integer number that refers to a specific object. In the CPython implementation, this number is the memory address of the object. The id of an object is used to help differentiate between two variables being identical and them being linked to the same object. We can use “==” to determine if they are identical, while “is” can be used to determine if both variables are pointing to the same object. If we have two lists x and y, we can see their id’s are unique:
But if we do this, things start to look weird:
“Whaaaattt???” you might say. “I thought two seconds ago those two lists were considered different? Why are these the same?” To answer that question, we must go deeper into the subject of mutability.
So, what is mutability? Simply said it is the ability to mutate, or alter, an object. Some built-in mutable types in Python are: lists, sets, dicts, and bytearrays. Let’s look at an example with our good friend the list. We can append items to out list:
We can remove items from out list:
We can even alter items in our list:
Like I said before, everything in Python is an object. So every element in the list is also an object. If we start looking a little closer, things start to get weird again:
Why did l’s id change? The list’s id didn’t change when we altered it’s values, so why would it’s element? It’s because integers and strings are not mutable, rather they are immutable.
Immutability is exactly what it sounds like: the opposite of mutability. This means that you cannot change the contents of the object. Some built-in immutable types are: ints, floats, complexes, strings, tuples, frozensets, and bytes. So in the above example, the list object initially contained references to the objects “1”, “2”, and “3”. Since we cannot change the object itself, the list changed which reference it held, such that it now contains a reference to the object “Hello”. In other words, a reassignment occurred. While we cannot append an immutable object, we can still do:
While it seems like we appended “Hello” to “ World”, what actually happened was something more along the lines of:
s = s + “ World”
The value of s, “Hello”, plus “ World” was calculated and s was given the reference to this new object. A similar process happens when adding numbers:
If you try to change an element in an immutable set, however. The interpreter will raise an exception:
Similarly, while you cannot actually remove an element in an immutable object, you can slice the object, which will return a new object containing the desired values:
Since Python must create a separate object for each unique immutable value, which takes up a lot of memory, the interpreter cleverly optimizes it’s creation of objects. It does so by using the same reference number for objects that cannot change, such as strings.
In the CPython interpretation, it goes one step further and stores all common small int objects (-5(inclusive) to 257(exclusive)) in an array for easy reference. Unlike other objects, which are created as needed and destroyed once there are no more variables referencing that object, these small int objects remain throughout the program.
Why Does It Matter?
When assigning variables, understanding the differences between mutable and immutable objects is key. Mutable objects have quite some tricks up their sleeves. In an earlier example we showed:
However, if we assign y like so:
How is y the same object as x? I thought they were different objects before? Well, the way in which assignments occurs in Python is crucial. In this case, we have not assigned a new object to y, but instead aliased it to x, meaning we gave it the same reference as x. Since both now point to the same object, a change in one will affect a change in another:
But what if you want to make a copy of your object without worrying about altering your original? Just as how slicing of immutable objects returns a new object, so does the slicing of lists:
Appending to lists can be a bit tricky as well. In Python, whatever is evaluated on the right side of the assignment expression is then made referenced by whatever is on the left side of the expression. Therefore:
A new list is created since the right side of the assignment was evaluated as [1, 2, 3, 4, 5] and that new object was made referenced by l. However, if we do:
We can see that the object remains the same. This is called assignment in-place, and is equivalent to appending to the list. However, since immutable objects cannot be changed, the expressions a+= b and = a + b would function the same, similar to our “Hello World” example above.
Passing Arguments to Functions:
In other programming languages, arguments are often said to be passed one of two ways: by value (a new variable is given the same value as the argument passed) or by reference (a variable holds a reference to the data, such that it could be changed). Python, however, is unique. Python passes the reference to the object. Similar to how we aliased x = y, the function’s argument is aliased to the object reference by the variable passed to the function. This means that if a reassignment occurs inside of the function, it will not alter the original variable passed to the function. Since immutable types cannot be altered any change to the variable inside of the function will not persist:
However, since lists are mutable, you can alter their contents:
Notice how the reference to the list remained the same throughout the program. If we were to reassign l inside the function, however, it would not affect the original list:
Happy coding :)