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

Sha*_*Han 164 python indexing swap list python-3.x

众所周知,pythonic 的方式来交换两个项目的值,a并且b

a, b = b, a
Run Code Online (Sandbox Code Playgroud)

它应该相当于

b, a = a, b
Run Code Online (Sandbox Code Playgroud)

但是,今天在写代码的时候,无意中发现下面的两个swap给出了不同的结果:

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]
Run Code Online (Sandbox Code Playgroud)

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

Sil*_*olo 138

来自python.org

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

...

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

所以我解释这意味着你的任务

nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
Run Code Online (Sandbox Code Playgroud)

大致相当于

tmp = nums[nums[i]-1], nums[i]
nums[i] = tmp[0]
nums[nums[i] - 1] = tmp[1]
Run Code Online (Sandbox Code Playgroud)

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

而另一个

nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
Run Code Online (Sandbox Code Playgroud)

就好像

tmp = nums[i], nums[nums[i]-1]
nums[nums[i] - 1] = tmp[0]
nums[i] = tmp[1]
Run Code Online (Sandbox Code Playgroud)

因此,在这两种情况下,首先评估右侧。但是然后左边的两块按顺序求值,求值后立即赋值。最重要的是,这意味着,在左侧第二任期的第一次分配后,仅在评估时已经完成。因此,如果您nums[i]先更新,则nums[nums[i] - 1]引用的索引与您nums[i]第二次更新的索引不同。

  • 举一个更简单的例子:如果你有 `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`。 (51认同)
  • 要么是数组欺骗,要么是疯狂的 `__getattr__` / `__setattr__` 乐趣。不过,数组欺骗可能要容易十倍。 (2认同)

tri*_*cot 71

这是因为评价-特别是在左侧的侧=-从左至右发生的情况:

nums[i], nums[nums[i]-1] =
Run Code Online (Sandbox Code Playgroud)

首先nums[i]被赋值,然后值用于确定赋值中的索引nums[nums[i]-1]

在做这样的任务时:

nums[nums[i]-1], nums[i] =
Run Code Online (Sandbox Code Playgroud)

... 的索引nums[nums[i]-1]取决于 的旧值nums[i],因为分配到nums[i]后面仍然遵循...

  • 数组已发生变异。使用突变数组中的值作为数组的索引将产生取决于突变执行顺序的结果。 (5认同)
  • @user253751,是的,但是OP的问题并不在于RHS。当*作业*(在 LHS 上)开始完成时,RHS 已经被评估。我的答案集中在左侧的分配序列上。 (3认同)

Thi*_*lle 34

这是根据规则发生的:

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

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

nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]
Run Code Online (Sandbox Code Playgroud)

相当于:

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
Run Code Online (Sandbox Code Playgroud)

并且由于现在评估右侧,分配等效于:

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

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

nums[2] = 3
nums[2] = 4
Run Code Online (Sandbox Code Playgroud)

这使:

print(nums)
# [1, 2, 4, 3]
Run Code Online (Sandbox Code Playgroud)

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

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]
Run Code Online (Sandbox Code Playgroud)


plu*_*ash 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]
Run Code Online (Sandbox Code Playgroud)

虽然你的第二个例子相当于

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]
Run Code Online (Sandbox Code Playgroud)

这与你得到的一致。


Guy*_*emi 7

为了理解评估的顺序,我创建了一个“变量”类,该类在设置和获取它的“值”时进行打印。

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
Run Code Online (Sandbox Code Playgroud)

当运行结果为:

b get 2
a get 1
a set 2
b set 1
Run Code Online (Sandbox Code Playgroud)

这表明首先评估右侧(从左到右),然后评估左侧(再次从左到右)。

关于 OP 的示例:右侧将在两种情况下评估为相同的值。左侧第一项已设置,这会影响第二项的评估。它从来没有同时进行过独立评估,只是大多数时候你看到它被使用,这些术语不相互依赖。在列表中设置一个值,然后从该列表中获取一个值以用作同一列表中的索引通常不是一件事,如果这很难理解,你就明白了。就像在 for 循环中更改列表的长度很糟糕一样,这也有同样的味道。(虽然是一个刺激性的问题,你可能已经猜到我跑到便笺簿了)