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

Understand Python swapping: why is a, b = b, a not always equivalent to b, a = a, b?

我们都知道,交换两个项目ab的值的pythonic方式是

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 交换中,两个分配同时独立发生。

这是因为评估——特别是在=侧——从左到右发生:

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

Assignment of an object to a target list, optionally enclosed in parentheses or square brackets, is recursively defined as follows.

...

  • Else: The object must be an iterable with the same number of items as there are targets in the target list, and the items are assigned, from left to right, to the corresponding targets.

所以我将其解释为您的作业

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]

所以在这两种情况下,首先评估右侧。但是然后左边的两块按顺序求值,求值后马上做作业。至关重要的是,这意味着左侧的第二项仅在第一个赋值 already 完成后才计算。因此,如果您首先更新 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]

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

与你得到的一致。

为了理解评估的顺序,我制作了一个 'Variable' class,它在设置和获取发生在它的 'value' 时打印。

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 使用并移除栈顶的三个值,一个要存储的值,一个对象,一个下标。 (return 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

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

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

所以肯定有问题。永远不要使用列表的内容作为该列表的索引。

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

在此代码中:

nums[nums[i]-1], nums[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])

也许 good 关于交换的问题,使用的是:

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