Search code examples
pythonpython-3.xlistmutability

Mutability of lists as it relates to the argument of a function


I am learning Python using Lutz's book. I am using Python 3.6.5 from Anaconda distribution. I did research this problem on SO and didn't find any thread that answers my question. Mutability of lists in python talks about append and not how we can pass a mutable object to a function.

My question is that when I make in-place changes to the list using index inside the function, the changes do get reflected, as expected because mutable objects are passed by reference. However, when I assign a list directly, the changes don't get reflected.

Specifically, I have created two lists L1 and L2. For L1, I would do assignment using index, but for L2, I would do direct assignment inside the function.

L1=[2]
L2=['a']
print("Before, L1:",L1)
print("Before, L2:",L2)
def f(a,b):
    a[0] =[3] #Using index-based assignment
    b = ['b'] #Direct assignment

#Pass L to f
f(L1,L2)
print("After, L1:",L1)
print("After, L2:",L2)

The output is:

Before, L1: [2]
Before, L2: ['a']
After, L1: [[3]]
After, L2: ['a']

As we can see, L1 got changed, but not L2.

Question: Can someone please explain why is it that the value of L2 doesn't get changed to 'b'?

If you believe this post is duplicate, it will be great if you tag the relevant post.


As an aside, I ran a small experiment to see whether there is anything to do with index-based assignment or direct assignment.

l=[2]
id(l)
l[0] = 3 #Index assignment
id(l) # Memory location doesn't change

l = 3 # direct assignment
id(l) #Memory location changes.

Hence, it seems that there is a concept I am missing, meaning I am unsure why direct assignment changes the memory location.


Solution

  • If we change your code slightly, we can use id to see how the references change (or don't change):

    L1=[2]
    L2=['a']
    print("Before, L1:", L1, id(L1))
    print("Before, L2:", L2, id(L2))
    def f(a,b):
        print("Inside, Before, a:", id(a))
        print("Inside, Before, b:", id(b))
        a[0] =[3] #Using index-based assignment
        b = ['b'] #Direct assignment
        print("Inside, After, a:", id(a))
        print("Inside, After, b:", id(b))
    
    #Pass L to f
    f(L1,L2)
    print("After, L1:", L1, id(L1))
    print("After, L2:", L2, id(L2))
    

    Output:

    Before, L1: [2]     1870498294152  # L1
    Before, L2: ['a']   1870498294280  # L2
    Inside, Before, a:  1870498294152  # L1
    Inside, Before, b:  1870498294280  # L2
    Inside, After, a:   1870498294152  # L1
    Inside, After, b:   1870498239496  # Something different, not L2
    After, L1: [[3]]    1870498294152  # L1
    After, L2: ['a']    1870498294280  # L2
    

    Note, the numbers aren't significant in themselves other than to help distinguish references to different objects. Running this yourself (or if I ran it again), would cause the ids to change.

    With a, you're modifying/mutating a but not attempting to re-assign the reference. That's fine.

    With b, you're re-assigning the reference. This will work inside the function (as the "Inside, After, b:" print call shows), but this change will not be reflected outside of the function. b will be restored to reference the original object, ['a'].

    As to why...

    meaning I am unsure why direct assignment changes the memory location.

    Inside your function, a and b are just references to objects. Initially, they reference (the objects referenced by) L1 and L2 respectively because by calling f, you're passing references to those objects.

    a[0] = [3] first dereferences a (or L1 in this case), then the [0] index, and sets that value.

    In fact, if you look at id(a[0]) before and after that call then that would change. a is a list of references. Try it:

    print(id(a[0]))   # One thing
    a[0] =[3] #Using index-based assignment
    print(id(a[0]))   # Something different
    

    This is fine. When you exit the function L1 will still reference the object that the function references using a and it's mutation at the 0-index will remain.

    With b = ['b'] you're reassigning or rebinding b to a new object. The old object still exists (for later use outside the function).

    Lastly, I use the term "reference" a lot, but Python is not precisely a "pass-by-reference" language, rather variable names are bound to objects. In the second, you're rebinding b, losing the association to the originally-referenced object L2 forever.