当使用pop()、list[-1]和+=时,Python的求值顺序是什么?

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

这将导致[1, 6]

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

这导致得出[4, 2]。哪种运算顺序会得出这两个结果?


3
以第一个例子为例。执行a.pop()会返回3并将a变为[1,2]。现在计算a[-1] = a[-1] + 3,你得到的结果是5,而不是6。 - Simd
4
我猜Python首先将a[-1] += a.pop()翻译为a[-1] = a[-1] + a.pop()。这就是为什么你得到了6。在执行a.pop()之前,先计算a[-1]。如果你将其改为a[-1] = a.pop() + a[-1],则会得到5 - Ma0
12
尽管此页面上的多个评论似乎表明相反的意思,请注意通常情况下 a += b 不等同于 a = a + b,且 Python 不能保证将其翻译为相同结果。不同的方法会被调用,是否最终得到相同结果取决于操作数的类型。 - Alex Riley
3
@Chris_Rands: 我在构思评论时,试图谨慎表达两者“一般而言”不等价的意思,并且这取决于对象:我同意在这种情况下它是合理的。(但另外一方面,我不确定 ++= 操作符在整数中等效这一事实是回答这个问题的关键:如果涉及不同对象,一般结果将是相同的。我认为评估顺序是主要要点。) - Alex Riley
2
注意:永远不要编写依赖于这些细节的代码。即使它能工作,也不是好的代码。如果你将副作用隔离在它们自己的行上(这大约相当于每行一个状态修改),阅读和修改你的代码会更容易。在这种情况下,你应该做 a = [1, 2, 3]; temp = a.pop(); a[-1] = 2 * temp 或者 a = [1, 2, 3]; temp = a.pop(); a[-1] += temp,具体取决于你想要做什么。这样可以明确你预期的评估顺序,并且更容易正确实现。 - jpmc26
显示剩余5条评论
5个回答

39

先计算右手边再计算左手边。在任何一边,计算顺序都是从左往右。

a[-1] += a.pop()a[-1] = a[-1] + a.pop()是等价的。

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

看一下当我们改变RHS操作的顺序时,行为如何改变。

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

4
在 RHS 中,从左到右计算。有趣的是,尽管运算符按照优先级顺序进行计算,但实际表达式显然并不是这样,例如在 f() + g() * h() 中,函数按照 f、g、h 的顺序进行计算。例如,对于 a = [3, 2, 1]a.pop() + a.pop() * a.pop() 进行计算会得出 7(即 1 + 2 * 3)。 - tobias_k
@tobias_k 嗯,* 运算符在 + 运算符之前被计算。将函数调用视为另一个具有比 * 更高优先级的运算符可能会有所帮助。 - Random832
@Random832,那部分很清楚;让我困惑的是,在(a+(b+(c+(d+(...))))中,首先评估a,然后是b等。但我想这只是我的“人类”视角,因为我会直观地解析表达式并确定要先评估最内部的部分,这样我就不必保留太多“内存”(字面上)。当然,计算机可以按从左到右的顺序评估表达式,并将中间结果推入堆栈。尽管如此,这仍然值得指出,因为其他人可能会有同样错误的直觉。 - tobias_k
@tobias_k:许多编程语言都这样做,但C和C++是明显的例外。 - Kevin
1
@Rob 在执行 a.pop(-1) 操作之前,a[-1] 已经被计算出来了,其值为 3。然后进行 pop 操作,同样得到值 3。因此,数学运算结果为 3+3。这是因为在右侧我们从左到右逐个计算每个部分,然后再处理数学运算。 - coderforlife
@tobias_k 在许多编程语言中的一般规则是使用括号来覆盖运算符优先级,但并不影响计算顺序。因此,它只影响不同操作的分组方式。因此,最里层的“+”将首先执行,但这并不意味着必须首先评估“d”。 - Barmar

22
关键洞察是 a[-1] += a.pop()a[-1] = a[-1] + a.pop() 的语法糖。这是因为 += 应用于不可变对象(这里是一个 int),而不是可变对象(相关问题在这里)。

右侧表达式(RHS)先被求值。在 RHS 中:等效的语法是 a[-1] + a.pop()。首先,a[-1] 获取最后一个值 3。其次,a.pop() 返回 33 + 36

在左侧表达式(LHS)中,由于已经应用了就地突变,因此 a 现在为 [1,2],因此 a[-1] 的值从 2 更改为 6


2
a[-1] += a.pop() 的简写形式是 a[-1] = a[-1] + a.pop(),这只有在 a 是整数列表时才成立,我现在已经学到了。值得一提。 - Simd
@felipa 好的,是的我添加了一个编辑,注意如果 a 是字符串或元组列表,它也会像整数一样表现出相同的行为(仅对可变对象不同)。 - Chris_Rands

16

让我们来看一下对于a[-1] += a.pop()1)的输出结果,使用dis.dis函数:

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)

不同指令的含义在此处列出

首先,LOAD_FASTLOAD_CONSTa-1 加载到栈上,DUP_TOP_TWO 在对两者进行复制之后,BINARY_SUBSCR 获取下标值,结果为栈上的 a, -1, 3。然后再次加载 aLOAD_ATTR 加载 pop 函数,CALL_FUNCTION 不带参数进行调用。此时,栈上的元素是 a, -1, 3, 3INPLACE_ADD 将前两个值相加。最后,ROT_THREE 将栈旋转为 6, a, -1,以匹配 STORE_SUBSCR 期望的顺序,并将值存储。

因此,简而言之,在调用 a.pop() 之前,计算当前的 a[-1] 的值,然后将加法的结果存储回新的 a[-1] 中,与其当前值无关。


1) 这是 Python 3 的反汇编结果,稍微压缩以更好地适应页面,添加了一列显示 # ... 后的栈;对于 Python 2,它看起来有点不同,但类似。


使用 dis.dis 实在是很酷的一件事情,以前从没听说过这个工具! - ppasler

6

使用一个细包装来包装一个带有调试打印语句的列表,可以用来展示你的情况下的评估顺序:

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

当我现在运行你的示例时:
>>> 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

首先获取列表中的最后一项,即3,然后弹出最后一项,也是3,将它们相加并用 6 覆盖列表的最后一项。因此,最终列表为 [1, 6]

在第二个例子中:

>>> 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

这将首先取出第一个项 (1),将其加上弹出的值(3),并用总和覆盖了第一个项: [4, 2]


计算顺序已经由@Fallen@tobias_k解释过了。本答案只是补充了那里提到的一般原则。


4

针对您的具体示例

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

排序:

  1. 在执行 = 操作后评估 a[-1]
  2. pop(),减少 a 的长度
  3. 加法操作
  4. 赋值操作

问题在于,在执行 pop() 操作后,a[-1] 变成了 a[1] 的值(之前是 a[2]),但这发生在赋值操作之前。

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

正常工作

  1. =后评估a[0]
  2. pop()
  3. 加法
  4. 赋值

此示例说明,为什么在操作列表时(通常是for循环)不应该对其进行修改。在这种情况下始终使用副本。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接