理解 Python 交换: 为什么 a,b = b,a 不总是等价于 b,a = a,b?

众所周知,交换两个项 ab的值的 Python 方法是

a, b = b, a

它应该相当于

b, a = a, b

然而,今天当我在编写一些代码时,我意外地发现以下两个交换产生了不同的结果:

nums = [1, 2, 4, 3]
i = 2
nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
print(nums)
# [1, 2, 4, 3]


nums = [1, 2, 4, 3]
i = 2
nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
print(nums)
# [1, 2, 3, 4]

这对我来说太难以置信了。有人能给我解释一下这里发生了什么吗?我认为在 Python 交换中,两个任务同时独立地发生。

21838 次浏览

这是因为评估——特别是在 =左边侧——是从左到右进行的:

nums[i], nums[nums[i]-1] =

首先分配 nums[i],然后使用 那个值确定分配给 nums[nums[i]-1]的索引

这样做作业的时候:

nums[nums[i]-1], nums[i] =

... nums[nums[i]-1]的指数取决于 nums[i]的旧值,因为 nums[i]的分配仍然跟随..。

来自 Python.org

将对象赋值给目标列表(可选用括号或方括号括起来) ,递归定义如下。

...

  • Else: 对象必须是可迭代的,其项数与目标列表中的目标数相同,并且从左到右将这些项分配给相应的目标。

所以我理解为你的任务

nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]

大致相当于

tmp = nums[nums[i]-1], nums[i]
nums[i] = tmp[0]
nums[nums[i] - 1] = tmp[1]

(当然,还有更好的错误检查)

而另一个呢

nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]

就像

tmp = nums[i], nums[nums[i]-1]
nums[nums[i] - 1] = tmp[0]
nums[i] = tmp[1]

所以在这两种情况下,首先对右侧进行评估。但是,然后两个部分的左侧被评估的顺序,和作业完成后立即评估。至关重要的是,这意味着只有在完成第一次赋值 已经之后,才能计算左边的第二项。因此,如果您首先更新 nums[i],那么 nums[nums[i] - 1]引用的索引与更新 nums[i]所引用的索引不同。

这是按照规则进行的:

  • 首先评估右侧
  • 然后,左边的每个值从左到右得到它的新值。

因此,对于 nums = [1, 2, 4, 3],第一种情况下的代码

nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]

等同于:

nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]


nums[2], nums[nums[2]-1] = nums[3], nums[2]


nums[2], nums[nums[2]-1] = 3, 4

由于右边已经计算过了,所以作业等价于:

nums[2] = 3
nums[nums[2]-1] = 4


nums[2] = 3
nums[3-1] = 4


nums[2] = 3
nums[2] = 4

它给出了:

print(nums)
# [1, 2, 4, 3]

在第二种情况下,我们得到:

nums[nums[2]-1], nums[2] = nums[2], nums[nums[2]-1]


nums[nums[2]-1], nums[2] = nums[2], nums[3]


nums[nums[2]-1], nums[2] = 4, 3


nums[nums[2]-1] = 4
nums[2] = 3


nums[4-1] = 4
nums[2] = 3


nums[3] = 4
nums[2] = 3
print(nums)
# [1, 2, 3, 4]

在表达式的左边,你同时读写数字[ i ] ,我不知道 python 是否保证了从左到右的解包操作的处理,但是让我们假设它做到了,你的第一个示例将等价于。

t = nums[nums[i]-1], nums[i]  # t = (3,4)
nums[i] = t[0] # nums = [1,2,3,3]
n = nums[i]-1 # n = 2
nums[n] = t[1] # nums = [1,2,4,3]

而你的第二个例子等价于

t = nums[i], nums[nums[i]-1]  # t = (4,3)
n = nums[i]-1 # n = 3
nums[n] = t[0] # nums = [1,2,4,4]
nums[i] = t[0] # nums = [1,2,3,4]

这和你得到的一致。

为了理解求值的顺序,我创建了一个“变量”类,它在设置并获得它的“值”时打印出来。

class Variable:
def __init__(self, name, value):
self._name = name
self._value = value


@property
def value(self):
print(self._name, 'get', self._value)
return self._value


@value.setter
def value(self):
print(self._name, 'set', self._value)
self._value = value


a = Variable('a', 1)
b = Variable('b', 2)


a.value, b.value = b.value, a.value

当运行结果:

b get 2
a get 1
a set 2
b set 1

这表明首先对右侧进行计算(从左到右) ,然后对左侧进行计算(同样是从左到右)。

关于 OP 的例子: 在这两种情况下,右边的值都是相同的。左侧第一学期设置,这影响了第二学期的评价。它从来没有同时和独立的评估,只是大多数情况下,你看到这个使用,术语不相互依赖。在列表中设置一个值,然后从该列表中获取一个值作为同一列表中的索引,这通常不是一件容易的事情,如果这很难理解,那么您可以理解。就像在 for 循环中改变列表的长度是不好的一样,这也有相同的味道。(刺激的问题,虽然你可能已经从我跑到一个便签簿猜到了)

分析 CPython 中代码段的一种方法是为其模拟堆栈机器分解字节码。

>>> import dis
>>> dis.dis("nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]")
1           0 LOAD_NAME                0 (nums)
2 LOAD_NAME                0 (nums)
4 LOAD_NAME                1 (i)


6 BINARY_SUBSCR
8 LOAD_CONST               0 (1)
10 BINARY_SUBTRACT
12 BINARY_SUBSCR
14 LOAD_NAME                0 (nums)
16 LOAD_NAME                1 (i)
18 BINARY_SUBSCR


20 ROT_TWO


22 LOAD_NAME                0 (nums)
24 LOAD_NAME                1 (i)
26 STORE_SUBSCR


28 LOAD_NAME                0 (nums)
30 LOAD_NAME                0 (nums)
32 LOAD_NAME                1 (i)
34 BINARY_SUBSCR
36 LOAD_CONST               0 (1)
38 BINARY_SUBTRACT
40 STORE_SUBSCR


42 LOAD_CONST               1 (None)
44 RETURN_VALUE

我添加了空白行,以使阅读更容易。这两个获取表达式以字节0-13和14-19计算。BINARY _ SUBSCR 用从对象获取的值替换堆栈上的前两个值: 对象和下标。两个提取的值进行交换,以便第一个计算值是第一个界限。这两个存储操作以字节22-27和28-41完成。STORE _ SUBSCR 使用并删除堆栈上的前三个值、要存储的值、对象和下标。(返回值显然没有添加任何部分。)这个问题的重要部分在于,商店的计算是按顺序分批进行的。

Python 中对 CPython 计算的最接近的描述需要引入一个堆栈变量

stack = []
stack.append(nums[nums[i]-1])
stack.append(nums[i])
stack.reverse()
nums[i] = stack.pop()
nums[nums[i]-1] = stack.pop()

下面是反向语句的反汇编

>>> dis.dis("nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]")
1           0 LOAD_NAME                0 (nums)
2 LOAD_NAME                1 (i)
4 BINARY_SUBSCR


6 LOAD_NAME                0 (nums)
8 LOAD_NAME                0 (nums)
10 LOAD_NAME                1 (i)
12 BINARY_SUBSCR
14 LOAD_CONST               0 (1)
16 BINARY_SUBTRACT
18 BINARY_SUBSCR


20 ROT_TWO


22 LOAD_NAME                0 (nums)
24 LOAD_NAME                0 (nums)
26 LOAD_NAME                1 (i)
28 BINARY_SUBSCR
30 LOAD_CONST               0 (1)
32 BINARY_SUBTRACT
34 STORE_SUBSCR


36 LOAD_NAME                0 (nums)
38 LOAD_NAME                1 (i)
40 STORE_SUBSCR


42 LOAD_CONST               1 (None)
44 RETURN_VALUE

在我看来,只有当列表的内容在列表索引的范围内时,才会发生这种情况。例如:

nums = [10, 20, 40, 30]

代码将失败:

>>> nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

所以,我明白了。永远不要使用列表的内容作为列表的索引。

蒂埃里确实给了一个很好的答案,让我说得更清楚一点,

在这个代码中:

nums[nums[i]-1], nums[i]
  • I 是2,
  • Nums [ nums [ i ]-1]是 nums [4-1] ,所以 nums [3] ,(值是3)
  • Nums [ i ]是 nums [2] ,(值是4)
  • 结果是: (3,4)

在这个代码中:

nums[i], nums[nums[i]-1]
  • Nums [ i ]是 nums [2]变成3(= > [1,2,3,3])
  • 但是 nums [ nums [ i ]-1]是 nums [ 4-1] ,而 nums [ 3-1] ,所以 nums [2]也变成(返回)4(= > [1,2,4,3])

也许 关于掉期交易的问题使用了:

nums[i], nums[i-1] = nums[i-1], nums[i]

试试看:

>>> print(nums)
>>> [1, 2, 4, 3]
>>> nums[i], nums[i-1] = nums[i-1], nums[i]
>>> print(nums)
>>> [1, 4, 2, 3]

冠心病