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

172
众所周知,Python中交换两个变量a和b的值的方式是
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中,两个赋值是同时且独立地进行的。

166
经过研究你的代码片段,我能给出最好的答案是“不要这样做”。我认为操作顺序是造成差异的关键,但哇,这太令人困惑了。 - nicomp
16
@nicomp的回答并不令人十分满意。我通常发现了解为什么某件事情运作方式,有助于我理解其他相关领域的知识。 - Mark Ransom
90
那就是为什么我把它作为评论添加了。 - nicomp
5
示例失败的原因是将交换后的数字用作列表的索引。请检查答案。 - Mark Ransom
47
众所周知,Python 中交换两个变量 a 和 b 的方式是使用以下代码:(原文有误)a, b = b, a但如果使用复杂表达式,则涉及到表达式中的运算顺序。 - GACy20
显示剩余17条评论
8个回答

144

来自python.org

将对象分配给目标列表,可选择地括在括号或方括号中,递归地定义如下。

...

  • 否则:对象必须是具有与目标列表中目标数量相同的项目数的可迭代对象,并且从左到右将项目分配给相应的目标。

所以我理解这意味着你的任务是:

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] 时不同。


53
举个简单的例子:如果你有 a = [2, 2, 2, 2, 2]b = 2,那么执行 a[b], b = 3, 4; print(a) 应该打印出 [2, 2, 3, 2, 2],因为在将 a[b] 更新为 3 之后,b 变成了 4。但是执行 b, a[b] = 4, 3; print(a) 应该打印出 [2, 2, 2, 2, 3],因为在将 a[b] 更新为 3 之前,b 已经变成了 4 - Stobor
@Shaun 重要的部分是早期的赋值必须修改后面赋值操作的索引。鉴于在Python中从函数返回lvalues不起作用(或者说它起作用吗?),这可能意味着唯一的选择就是使用这种数组技巧。 - Voo
2
无论是数组技巧还是疯狂的__getattr__/__setattr__乐趣。虽然数组技巧可能容易十倍,但两者都可行。 - Silvio Mayolo
你的最后一句话是整个答案的关键。完美。 - simpleuser
1
交换操作顺序可能在不同的Python实现中被反转,导致错误重新出现,因此最好明确地执行所有潜在的副作用交换! - Infernoz

72

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

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]的赋值仍然在之后进行...


5
数组已被改变。使用经过改变的数组中的值作为索引将产生取决于变异执行顺序的结果。 - Bae
3
@user253751,是的,但OP的问题并不是RHS。当左侧的赋值(LHS)开始进行时,RHS已经被评估过了。我的答案侧重于左侧的赋值序列。 - trincot

34

这是根据规则发生的:

  • 首先评估右侧
  • 然后,从左到右,对左侧的每个值都分配新值。

因此,对于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]

11

在表达式的左侧,您既读取又写入了nums[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]

这与您得到的结果一致。

7
为了理解评估的顺序,我创建了一个“ Variable ”类,它会在其“值”被设置和获取时打印。
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

这表明右侧先被评估(从左到右),然后再评估左侧(同样从左到右)。

关于提问者的示例: 在两种情况下,右侧将评估为相同的值。左侧的第一个术语是设置的,这影响了第二个术语的评估。它从未同时独立地进行评估,只是大多数情况下,这些术语不相互依赖。在列表中设置一个值,然后取出该列表中的值用作索引,在同一列表中使用通常不是一件好事。如果这很难理解,就像在for循环中更改列表的长度一样,也具有同样的气味。(虽然这是一个刺激性的问题,正如你可能已经从我跑到草稿本上看到的那样)


4

分析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使用并删除堆栈上的顶部三个值,一个要存储的值,一个对象和一个下标。(返回None部分显然总是添加在末尾。) 对于此问题而言重要的部分是,这些存储的计算是按顺序分别在独立的批次中完成的。

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

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

1

我认为只有当列表的内容在列表索引范围内时才会出现这种情况。例如:

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

所以肯定是一个陷阱。永远不要使用列表的内容作为该列表的索引。


1
请使用代码框而不是块引用。 - I_love_vegetables

0

Thierry 给出了一个很好的答案,让我更清楚一些。请注意,如果 nums = [1, 2, 4, 3],

在这段代码中:

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]

ChD


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