sada haruna
sada haruna

Reputation: 33

List Mutation on Python

I tried mutating a list by swapping a common element between the list and another reference list with the first element. The implementation is as shown below:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> L[0], L[L.index(A[0])] = L[L.index(A[0])], L[0] #want to swap 3 with 1  
>>> L 
[1,2,3,4,5,6,7,8,9,] #List L was not mutated  

The list was not mutated as I anticipated. But when I modify the implementation as shown below, it worked:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> i = L.index(A[0])
>>> L[0], L[i] = L[i], L[0]
>>> L
[3,2,1,4,5,6,7,8,9,] #Now list mutated as desired even though L[i] and L[L.index(A[0])] evaluate to same value.  

My question is, why couldn't the first assignment mutate the list? I thought of it but my brain coudnt explain it.

Upvotes: 3

Views: 5347

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1123730

Although in Python, the right-hand side is evaluated first when doing multiple assignments, the left-hand assigment targets, if they have expressions in them, are evaluated one by one when assigning.

If instead, they'd be evaluated as assignment targets first as you appear to expect, this would of course work.

This is documented in the assignment statements section:

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.

and

If the target list is a comma-separated list of targets: The object must be an iterable with the same number of items as there are targets in the target list, and the items are assigned, from left to right, to the corresponding targets.

Emphasis mine. The left-to-right is crucial here. L[0] is assigned to before the L[L.index(3)] is assigned to.

The documentation then describes in detail what happens to a subscription target such as L[0] and L[L.index(3)]:

If the target is a subscription: The primary expression in the reference is evaluated. It should yield either a mutable sequence object (such as a list) or a mapping object (such as a dictionary). Next, the subscript expression is evaluated.

Again, emphasis mine; the subscript expression is evaluated separately, and since the target list is evaluated from left-to-right, that evaluation takes place after the previous assignment to L[0].

You can see this by disassembling the python code:

>>> import dis
>>> def f(L):
...     L[0], L[2] = L[2], L[0]
... 
>>> def g(L):
...     L[0], L[L.index(3)] = L[L.index(3)], L[0]
... 
>>> dis.dis(f)
  2           0 LOAD_FAST                0 (L)   # L[2]
              3 LOAD_CONST               1 (2)
              6 BINARY_SUBSCR       
              7 LOAD_FAST                0 (L)   # L[0]
             10 LOAD_CONST               2 (0)
             13 BINARY_SUBSCR       
             14 ROT_TWO             
             15 LOAD_FAST                0 (L)   # Store in L[0]
             18 LOAD_CONST               2 (0)
             21 STORE_SUBSCR        
             22 LOAD_FAST                0 (L)   # Store in L[2]
             25 LOAD_CONST               1 (2)
             28 STORE_SUBSCR        
             29 LOAD_CONST               0 (None)
             32 RETURN_VALUE        
>>> dis.dis(g)
  2           0 LOAD_FAST                0 (L)   # L[L.index(3)]
              3 LOAD_FAST                0 (L)
              6 LOAD_ATTR                0 (index)
              9 LOAD_CONST               1 (3)
             12 CALL_FUNCTION            1
             15 BINARY_SUBSCR       
             16 LOAD_FAST                0 (L)  #  L[0]
             19 LOAD_CONST               2 (0)
             22 BINARY_SUBSCR       
             23 ROT_TWO             
             24 LOAD_FAST                0 (L)  # Store in L[0]
             27 LOAD_CONST               2 (0)
             30 STORE_SUBSCR        
             31 LOAD_FAST                0 (L)  # Store in L[L.index(3)]
             34 LOAD_FAST                0 (L)
             37 LOAD_ATTR                0 (index)
             40 LOAD_CONST               1 (3)
             43 CALL_FUNCTION            1
             46 STORE_SUBSCR        
             47 LOAD_CONST               0 (None)
             50 RETURN_VALUE        

The storing operation first stores L[0] = 3, so the next call to L.index(3) returns 0 and the 1 is thus stored right back in position 0!

The following does work:

L[L.index(3)], L[0] = L[0], L[L.index(3)]

because now the L.index(3) lookup is done first. However, it's best to store the result of an .index() call in a temporary variable as not calling .index() twice is going to be more efficient in any case.

Upvotes: 12

Gareth Latty
Gareth Latty

Reputation: 89077

The problem is that the two are not equivalent. The first example is akin to doing:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> i = L.index(A[0])
>>> L[0] = L[i]
>>> i = L.index(A[0])
>>> L[i] = L[0]

This means that you end up swapping, then finding the element you just swapped and swapping back.

The reason you are confused is that you are thinking of the tuple assignment as Python doing both things at the same time - this isn't how the execution is performed, it is performed in an order, which changes the outcome.

It's worth noting that even if it did work, it would be a sub-optimal way of doing this. list.index() isn't a particularly fast operation, so doing it twice for no reason isn't a great idea.

Upvotes: 7

Related Questions