a = [1, 2, 3]
a[-1] += a.pop()
This results in [1, 6]
.
a = [1, 2, 3]
a[0] += a.pop()
This results in
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()
return
s 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
.
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.
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]
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.
For you specific example
a[-1] += a.pop() #is the same as
a[-1] = a[-1] + a.pop() # a[-1] = 3 + 3
Order:
a[-1]
after =
pop()
, decreasing the length of a
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
a[0]
after =
pop()
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.