What is the order of evaluation in python when using pop(), list[-1] and +=?

Question:

a = [1, 2, 3]
a[-1] += a.pop()

This results in [1, 6].

a = [1, 2, 3]
a[0] += a.pop()

This results in [4, 2]. What order of evaluation gives these two results?

Asked By: graffe

||

Answers:

The key insight is that a[-1] += a.pop() is syntactic sugar for a[-1] = a[-1] + a.pop(). This holds true because += is being applied to an immutable object (an int here) rather than a mutable object (relevant question here).

The right hand side (RHS) is evaluated first. On the RHS: equivalent syntax is a[-1] + a.pop(). First, a[-1] gets the last value 3. Second, a.pop() returns 3.
3 + 3 is 6.

On the Left hand side (LHS), a is now [1,2] due to the in-place mutation already applied by list.pop() and so the value of a[-1] is changed from 2 to 6.

Answered By: Chris_Rands

For you specific example

a[-1] += a.pop() #is the same as 
a[-1] = a[-1] + a.pop() # a[-1] = 3 + 3

Order:

  1. evaluate a[-1] after =
  2. pop(), decreasing the length of a
  3. addition
  4. assignment

The thing is, that a[-1] becomes the value of a[1] (was a[2]) after the pop(), but this happens before the assignment.

a[0] = a[0] + a.pop() 

Works as expected

  1. evaluate a[0] after =
  2. pop()
  3. addition
  4. assignment

This example shows, why you shouldn’t manipulate a list while working on it (commonly said for loops). Always work on copys in this case.

Answered By: ppasler

RHS first and then LHS. And at any side, the evaluation order is left to right.

a[-1] += a.pop() is same as, a[-1] = a[-1] + a.pop()

a = [1,2,3]
a[-1] = a[-1] + a.pop() # a = [1, 6]

See how the behavior changes when we change the order of the operations at RHS,

a = [1,2,3]
a[-1] = a.pop() + a[-1] # a = [1, 5]
Answered By: Fallen

Let’s have a look at the output of dis.dis for a[-1] += a.pop()1):

3    15 LOAD_FAST            0 (a)                             # a,
     18 LOAD_CONST           5 (-1)                            # a, -1
     21 DUP_TOP_TWO                                            # a, -1, a, -1
     22 BINARY_SUBSCR                                          # a, -1, 3
     23 LOAD_FAST            0 (a)                             # a, -1, 3, a
     26 LOAD_ATTR            0 (pop)                           # a, -1, 3, a.pop
     29 CALL_FUNCTION        0 (0 positional, 0 keyword pair)  # a, -1, 3, 3
     32 INPLACE_ADD                                            # a, -1, 6
     33 ROT_THREE                                              # 6, a, -1
     34 STORE_SUBSCR                                           # (empty)

The meaning of the different instructions is listed here.

First, LOAD_FAST and LOAD_CONST load a and -1 onto the stack, and DUP_TOP_TWO duplicates the two, before BINARY_SUBSCR gets the subscript value, resulting in a, -1, 3 on the stack. It then loads a again, and LOAD_ATTR loads the pop function, which is called with no arguments by CALL_FUNCTION. The stack is now a, -1, 3, 3, and INPLACE_ADD adds the top two values. Finally, ROT_THREE rotates the stack to 6, a, -1 to match the order expected by STORE_SUBSCR and the value is stored.

So, in short, the current value of a[-1] is evaluated before calling a.pop() and the result of the addition is then stored back to the new a[-1], irrespective of its current value.


1) This is the disassembly for Python 3, slightly compressed to better fit on the page, with an added column showing the stack after # ...; for Python 2 it looks a bit different, but similar.

Answered By: tobias_k

Using a thin wrapper around a list with debugging print-statements can be used to show the order of evaluation in your cases:

class Test(object):
    def __init__(self, lst):
        self.lst = lst

    def __getitem__(self, item):
        print('in getitem', self.lst, item)
        return self.lst[item]

    def __setitem__(self, item, value):
        print('in setitem', self.lst, item, value)
        self.lst[item] = value

    def pop(self):
        item = self.lst.pop()
        print('in pop, returning', item)
        return item

When I now run your example:

>>> a = Test([1, 2, 3])
>>> a[-1] += a.pop()
in getitem [1, 2, 3] -1
in pop, returning 3
in setitem [1, 2] -1 6

So it starts by getting the last item, which is 3, then pops the last item which is also 3, adds them and overwrites the last item of your list with 6. So the final list will be [1, 6].

And in your second case:

>>> a = Test([1, 2, 3])
>>> a[0] += a.pop()
in getitem [1, 2, 3] 0
in pop, returning 3
in setitem [1, 2] 0 4

This now takes the first item (1) adds it to the popped value (3) and overwrites the first item with the sum: [4, 2].


The general order of evaluation is already explained by @Fallen and @tobias_k. This answer just supplements the general principle mentioned there.

Answered By: MSeifert