元组内成员的交换 (a,b)=(b,a) 如何在内部工作?

41
In [55]: a = 5

In [56]: b = 6

In [57]: (a, b) = (b, a)

In [58]: a
Out[58]: 6

In [59]: b
Out[59]: 5

这个交换a和b的值是如何在内部工作的?它绝不是使用一个临时变量。


2
你可能会对使用dis查看代码的反汇编感兴趣。剧透:使用了字节码指令ROT_TWO - Kevin
1个回答

96
Python将右侧表达式与左侧赋值分开。首先计算右侧表达式,结果存储在堆栈上,然后使用从堆栈中获取的操作码对左侧名称进行赋值。
对于具有2或3个项目的元组赋值,Python直接使用堆栈:
>>> import dis
>>> def foo(a, b):
...     a, b = b, a
... 
>>> dis.dis(foo)
  2           0 LOAD_FAST                1 (b)
              3 LOAD_FAST                0 (a)
              6 ROT_TWO             
              7 STORE_FAST               0 (a)
             10 STORE_FAST               1 (b)
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        

在两个 LOAD_FAST 操作码(将变量的值推送到堆栈上)之后,堆栈顶部保存着 [a, b]ROT_TWO 操作码 交换堆栈上的前两个位置,因此堆栈顶部现在是 [b, a]。然后,两个 STORE_FAST 操作码 将这两个值存储在赋值左侧的名称中。第一个 STORE_FAST 弹出堆栈顶部的值并将其放入 a 中,接下来再次弹出并将该值存储在 b 中。需要旋转是因为 Python 保证在左侧目标列表中的赋值是从左到右完成的。
对于一个由3个名称组成的赋值语句,先执行ROT_THREE,然后再执行ROT_TWO,以将栈顶的前三个元素反转。
对于更长的左侧赋值语句,会显式地构建一个元组。
>>> def bar(a, b, c, d):
...     d, c, b, a = a, b, c, d
... 
>>> dis.dis(bar)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 LOAD_FAST                2 (c)
              9 LOAD_FAST                3 (d)
             12 BUILD_TUPLE              4
             15 UNPACK_SEQUENCE          4
             18 STORE_FAST               3 (d)
             21 STORE_FAST               2 (c)
             24 STORE_FAST               1 (b)
             27 STORE_FAST               0 (a)
             30 LOAD_CONST               0 (None)
             33 RETURN_VALUE        

这里使用包含[d, c, b, a]的堆栈来构建元组(按相反顺序,BUILD_TUPLE 再次从堆栈中弹出,将结果元组推送回堆栈),然后 UNPACK_SEQUENCE 再次从堆栈中弹出元组,并将所有元素从元组中推送回堆栈以进行 STORE_FAST 操作。

后者可能看起来像一种浪费的操作,但是赋值语句右侧可能是完全不同的东西,例如调用一个生成元组的函数,因此Python解释器不会做出任何假设,并始终使用UNPACK_SEQUENCE操作码。即使对于两个或三个名称的赋值操作,但稍后(peephole)优化步骤也会将具有2个或3个参数的BUILD_TUPLE/UNPACK_SEQUENCE组合替换为上述的ROT_TWOROT_THREE操作码以达到更高的效率。


5
+1 表示同意,“Python保证了左侧目标列表中的赋值是从左到右执行的”,因此需要进行旋转操作。我之前并不知道这一点。 - treehouse
1
这个解释的ELI5是什么? - JoeTheShmoe
我们能否将在堆栈上存储的两个值视为两个临时变量之间的关系?我知道这可能在技术上不正确,但只是为了理解而已。 - Sreenikethan I
@JoeTheShmoe:如果你只有5岁,我会说:Python需要进行一些复杂的协调才能把所有东西放在正确的位置上(也许,如果我能的话,我可以玩几个球来演示。直到你掌握了技巧,杂耍也是很复杂的,而一个5岁的孩子很可能会非常惊叹,无论我是否能够杂耍)。 - Martijn Pieters
1
@SreenikethanI:栈基本上是所有临时变量,所以是的。栈就像Python用来执行操作的草稿本。 - Martijn Pieters
非常好的答案,谢谢!供参考,这是C代码“ROT_TWO”的链接,至少在Python3.9中是如此。显然,在更新的版本中,它已被更通用的“SWAP”所取代。 - Eric Duminil

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