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

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

a, b = b, a

它应该相当于

b, a = a, b

但是,今天在写代码的时候,无意中发现下面的两个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]

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

回答

来自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]第二次更新的索引不同。

  • As a simpler example: If you have `a = [2, 2, 2, 2, 2]` and `b = 2` then
    `a[b], b = 3, 4; print(a)` should print `[2, 2, 3, 2, 2]` because `b` becomes `4` after `a[b]` is updated to `3` but
    `b, a[b] = 4, 3;
    print(a)`

    should print `[2, 2, 2, 2, 3]` because `b` becomes `4` before `a[b]` is updated to `3`.

  • Either array trickery or insane amounts of `__getattr__` / `__setattr__` fun. Array trickery is probably ten times easier, though.

回答

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

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]后面仍然遵循...

  • The array has been mutated. Using values from the mutated array as an index into the array will have results that depend upon the order of execution of the mutations.
  • @user253751, yes, but the OP's problem was not so much with the RHS. The RHS has already been evaluated when the *assignments* (on the LHS) start to be done. My answer focusses on that left side assignment sequence.

回答

这是根据规则发生的:

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

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


回答

在你的表达式的左侧,你同时在读和写 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]

这与你得到的一致。


回答

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

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 循环中更改列表的长度很糟糕一样,这也有同样的味道。(虽然是一个刺激性的问题,你可能已经猜到我跑到便笺簿了)


以上是理解Python交换:为什么a,b=b,a并不总是等价于b,a=a,b?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>