9. References#

In programming, particularly in languages like Python, lists are mutable objects. This means that when you pass a list to a function (as a parameter), you’re passing a reference to the original list, not a copy of it. This has significant implications, particularly in terms of side effects. Understanding these effects is crucial for writing reliable and maintainable code.

9.1. Variable and immutable values#

In Python, variables do not directly contain values as they might in some other programming languages. Instead, a variable holds a reference to a value that is stored somewhere in memory. To better understand this concept, imagine that the Python interpreter manages two distinct spaces:

The Namespace (Left Space): This is the space that is directly accessible to the user. It contains the variables that have been defined in your program. You can think of it as a map where variable names are the keys, and these keys point to objects in the memory.

The Memory Space (Right Space): This is where the actual values are stored. This space is not directly accessible to the user but is managed internally by the interpreter. When you assign a value to a variable, the variable in the namespace becomes a reference or pointer to an object in this memory space.

Consider the following assignment:

x = 5

Here’s what happens behind the scenes:

  • The interpreter creates an integer object with the value 5 in the memory space.

  • A variable x is created in the namespace and is assigned a reference to the memory location where the value 5 is stored.

../_images/8c2f49e480bf53f5e9d41d27947585830886615170c5e7d68d5979297f48e181.svg

Let’s continue with y = 10.

../_images/68bc40521b9ecc3ffdbe42b76cfd2025352c3436ca74f0158d8f2bf5e380aacb.svg

What happens when we execute the following instruction y = x? The right-hand side of the equation is evaluated first. We retrieve the value associated with the variable x. Then, the variable y will be assigned this same value. Both variables will point to the same data. The value 10 is no longer referenced and will eventually be removed from memory by a mechanism known as the garbage collector (see Garbage Collector).

../_images/6cde30b7eb5e3bff6066cf91aeeb06c6b48a18db1546d494b4f9eb735e8d630b.svg

Let’s now assume we define a function inc. It takes a parameter and returns the value of this parameter incremented by 1.

def inc(x):
    x = x+1
    return x

What is the effect of this definition on memory? As we have seen, functions can be manipulated like variables, and everything works in a similar way. A variable inc is created in the namespace, and a function object is added to the memory space.

../_images/09e1ee46f50482f77c8a679b119ac88cbf29b692ce4bf1895c16da11b62e8da9.svg

When we call the inc function, the interpreter retrieves the object associated with the inc variable. This object contains the instructions that will be executed. Let’s try to observe with our diagram what happens when the instruction x = inc(x) is called…

First, a local name space is created when the function is called. The parameter x is created and assigned the value 5. This parameter is a local variable that masks the x variable in the global name space.

../_images/ffa3f4d2f21ba5d1bd1746c7fd2d28292af8ae2badcd1a9af5e551582ff070cd.svg

After executing the first line of the function x = x+1, we obtain the following diagram.

../_images/ff6272a441120bc843f427ec3f4d3f99d2e5f474d04e2e0ef38804d627496546.svg

Let’s recall that we are observing the execution of the following instruction.

x = 5
y = x

def inc(x):
    x = x+1
    return x

x = inc(x)

At the moment the return statement is executed, the value associated with the local variable x is used to replace the function call inc(x). The global variable x is then assigned the value referenced by the local variable x, which is 6. Finally, the local memory namespace of the function call is destroyed.

../_images/65dc6c2f6c172daba6f9dfa0b9c8e918fde2b6c425c193c4b2bac4d26fe1ce72.svg

So far, everything is relatively clear, and there are no issues because values like 5, 6, or 10 are immutable…

9.2. Variable and Mutable Values#

If everything we’ve seen is clear, then the following should be as well, but you should understand why we need to be cautious with mutable values and function calls.

Let’s imagine we have the following code and use the same diagram to observe memory behavior.

l = [0, 1, 2, 3]
h = l

def inc(k):
    k[0] += 1
    
inc(l)

After executing the first three instructions, we have the following state.

../_images/fb8a4a295d786e337f814df051baaf886aa10d4704a810d4e8f8dddc664d77a5.svg

When the function inc(l) is called, a local variable is created, and it is clear that the only existing list is modified. The value of the first element changes from 0 to 1.

../_images/b2dce6cdb6715e8f4b83661f843d2f944f5ea2fd22674bf98d8c480607c77f2b.svg

If we want different behavior, we need to start the inc function by duplicating the list (using a slice) and, importantly, return the new list. We obtain the following code.

l = [0, 1, 2, 3]
h = l

def inc(k):
    k = k[:]
    k[0] += 1
    return k
    
l = inc(l)

After executing this code, l = [1, 1, 2, 3] and h = [0, 1, 2, 3].

../_images/8af0bff39e3e44c4951537d768bb72cec8332a2c1a3fb1762730a236f29faf76.svg

9.3. Fonction id#

A good way to observe whether lists are duplicated or not is to use the id function. This function allows you to find out the identifier of an object (see Object ID). Without going into details, the identifier of an object is unique and can be considered to correspond to its memory address.

>>> l = [0, 1, 2, 3] # l associated with a new list
>>> id(l)            # the id of the list 
4332163136
>>> h = l            # h is associated to the same list
>>> id(h)            # the id is obviouly the same...
4332163136
>>> l[0] = 1         # object is mutable
>>> id(l)            # id is not changed
4332163136
>>> h                # the two variables refer to the same list
[1, 1, 2, 3]
>>> l = l[:]         # duplication
>>> id(l)
4333099776           # the id is not the same, it is an other list, even if the value are the same
>>> l[0] = 100       # only one list is modified
>>> l, h
([100, 1, 2, 3], [1, 1, 2, 3])