Code along video
Learning objectives
Throughout this course, we have thought about some types (like integers) as passed by value and others (like lists) as passed by reference. In this code along, we will see that both behaviors come from a single idea: in Python, everything is an object.
Code along summary
Setup
We will continue working in python/src/shapes_methods/main.py from the previous code along. All of the new code in this lesson goes beneath the shape classes.
Value versus reference
So far, we have used mutability to decide how a variable behaves when we pass it into a function. Immutable types behave as though they are passed by value (the function cannot change them), and mutable types behave as though they are passed by reference (the function can change them). We can summarize this in a comment at the bottom of main.py.
# Value (immutable) Reference (mutable) # int lists # floats dictionaries # strings user defined classes # tuples etc.
This table has served us well, but Python experts would say that it is not quite how the language works. In Python, everything is an object, and everything is passed by object reference. Let’s see what that means.
Object identifiers and lists
Because everything in Python is an object, Python assigns every object an identifier, which you can think of as a number, and which we can access with the built-in function id(). Let’s write a function change_first() that takes a list of integers and changes its first element, printing the list and its identifier both before and after the change.
def change_first(a: list[int]) -> None:
print("Inside before:", a, id(a))
a[0] = 1
print("Inside after:", a, id(a))
In main(), we create a list of five zeros and print it, along with its identifier, before and after calling change_first().
lst = [0] * 5
print("Outside before:", lst, id(lst))
change_first(lst)
print("Outside after:", lst, id(lst))
STOP: We callid()four times on our list: outside the function before the call, inside the function before and after the change, and outside the function after the call. How many different identifiers do you think will be printed?
When we run our code, we see the same identifier four times (your number will differ from ours).
Outside before: [0, 0, 0, 0, 0] 4316760960 Inside before: [0, 0, 0, 0, 0] 4316760960 Inside after: [1, 0, 0, 0, 0] 4316760960 Outside after: [1, 0, 0, 0, 0] 4316760960
This is proof of what we have been calling pass by reference. Inside the function the list is called a, and outside it is called lst, but these are just two names for the very same object.
Integers are objects too
If everything is an object, then an integer is an object too. So let’s run the same experiment with an integer. We will write a function change_value() that takes an integer x and, for now, just prints its value and identifier.
def change_value(x: int) -> None:
print("Inside before:", x, id(x))
In main(), we set an integer n equal to 5, and print its value and identifier before calling the function.
n = 5
print("Outside before:", n, id(n))
change_value(n)
STOP: Integers are immutable, so we have always thought of them as passed by value. Will the two identifiers printed fornandxbe the same or different?
If you said different, then you were following everything we have said so far: an integer is passed by value, so the function should get a copy. And yet the two identifiers are the same.
Outside before: 5 4327927872 Inside before: 5 4327927872
When we pass n into the function, Python does not copy it. Inside the function, x is a second name for the exact same integer object. So what happens if we change x? Let’s add x += 10 to change_value(), followed by one more print statement, and print n again in main() after the function call.
def change_value(x: int) -> None:
print("Inside before:", x, id(x))
x += 10
print("Inside after:", x, id(x))
n = 5
print("Outside before:", n, id(n))
change_value(n)
print("Outside after:", n, id(n))
When we run this code, we see that three of the identifiers match, but the identifier of x after the change is new.
Outside before: 5 4396937280 Inside before: 5 4396937280 Inside after: 15 4396937600 Outside after: 5 4396937280
Here is what is happening. Because an integer object can never change, x += 10 cannot modify the object that x and n share. Instead, Python creates a new integer object with value 15 and makes x refer to it. The new object only appears at the moment of the change; up until then, nothing was copied. Meanwhile, n still refers to the original object, which is why n is still 5 outside the function.
So there is really only one behavior: every variable refers to an object, and passing a variable into a function passes a reference to that object. Mutable and immutable objects only look different because a mutable object can be changed in place, whereas an immutable one must be replaced by a new object whenever we try to change it.
Python’s dark secret: integer caching
Integers are even stranger than this. Let’s declare a second variable y equal to 5, and check whether it has the same identifier as n. Surely two different variables that we declared separately will not be the same object.
y = 5
if id(y) == id(n):
print("No way, Python.")
And yet when we run this code, we see No way, Python. printed. (The same thing happens if we declare two variables s and t that are both equal to "Hi".)
Behind the scenes, Python relies on a lower-level language called C, and the C implementation includes optimizations that make Python faster. One of these optimizations is that when we set y equal to 5, Python notices that it already has an integer object equal to 5, and rather than creating a second container holding 5, it lets n and y both refer to the same one. In fact, CPython goes a step further and pre-allocates a small range of commonly used integers in memory when it starts up.
Is this dangerous? What if we change y?
y += 1
if id(y) == id(n):
print("This would be very bad.")
Fortunately, This would be very bad. is not printed. As soon as we change y, Python makes y refer to a new object equal to 6, which decouples it from n. You could program in Python for years without realizing that this is happening behind the scenes, but now you know.
Looking ahead
We have learned that every Python variable refers to an object, and that the difference between mutable and immutable objects is whether that object can be changed in place. In the next code along, we will put our knowledge of objects to work by designing the data structures that we will need to represent evolutionary trees.