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

Posted

技术标签:

【中文标题】理解 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?理解 Python 交换:为什么 a, b = b, a 并不总是等价于 b, a = a, b? 【发布时间】:2021-09-10 03:08:51 【问题描述】:

众所周知,交换两个项目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 交换中,这两个分配同时独立发生。

【问题讨论】:

在研究了您的代码 sn-p 之后,我能给出的最佳答案是“不要那样做”。操作顺序是我认为的不同之处,但哇,这令人困惑。 @nicomp 这不是一个非常令人满意的答案。我经常发现,了解某事以它的方式运作的原因有助于我在其他相关领域。 这就是我添加它作为评论的原因。 @Ram 失败的例子是使用交换的数字作为列表的索引。检查答案。 “众所周知,交换两个项目 a 和 b 的 Python 方式是”,不,这就是交换 两个变量 的方式。如果您使用复杂的表达式,评估顺序就会发挥作用。 【参考方案1】:

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

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

【讨论】:

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

来自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] 时所指的索引不同。

【讨论】:

作为一个更简单的例子:如果你有a = [2, 2, 2, 2, 2]b = 2,那么a[b], b = 3, 4; print(a)应该打印[2, 2, 3, 2, 2],因为ba[b]更新为3之后变成4b, a[b] = 4, 3; print(a) 应该打印[2, 2, 2, 2, 3],因为ba[b] 更新为3 之前变为4 @Shaun 重要的是前面的赋值要修改后面的赋值操作的索引。鉴于从函数返回左值在 Python 中不起作用(或者它是否起作用?),这可能意味着唯一的选择就是这个数组诡计。 数组诡计或疯狂数量的__getattr__ / __setattr__ 乐趣。不过,数组欺骗可能要容易十倍。 你的最后一句话是整个答案的关键。完美。 在不同的 python 实现中,交换操作顺序可能会颠倒,从而导致错误复活,因此最好明确地进行所有潜在的副作用交换!【参考方案3】:

这是按照规则发生的:

首先评估右手边 然后,左侧的每个值都会从左到右获取其新值。

所以,使用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]

【讨论】:

【参考方案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]

这与你得到的一致。

【讨论】:

【参考方案5】:

为了理解评估的顺序,我创建了一个“变量”类,它在集合和发生时打印它的“值”。

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 循环中更改列表的长度是不好的一样,这具有相同的气味。 (不过,这是一个令人兴奋的问题,你可能已经猜到我跑到便笺簿上)

【讨论】:

【参考方案6】:

在 CPython 中分析代码 sn-ps 的一种方法是为其模拟堆栈机器反汇编其字节码。

>>> 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

我添加了空白行以使阅读更容易。两个 fetch 表达式在字节 0-13 和 14-19 中计算。 BINARY_SUBSCR 用从对象中获取的值替换堆栈上的顶部两个值,一个对象和下标。交换两个获取的值,以便计算的第一个值是第一个边界。这两个存储操作在字节 22-27 和 28-41 中完成。 STORE_SUBSCR 使用和删除堆栈上的前三个值、要存储的值、对象和下标。 (return None 部分显然总是添加到最后。)问题的重要部分是商店的计算是在单独和独立的批次中按顺序完成的。

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

【讨论】:

【参考方案7】:

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

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

当然,一个问题。永远不要使用列表的内容作为该列表的索引。

【讨论】:

请使用代码围栏而不是块引用【参考方案8】:

蒂埃里确实给出了一个很好的答案,让我更清楚。请注意,如果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]

心脏病

【讨论】:

以上是关于理解 Python 交换:为啥 a, b = b, a 并不总是等价于 b, a = a, b?的主要内容,如果未能解决你的问题,请参考以下文章

为啥是“a^=b^=a^=b;”不同于“a^=b;b^=a;a^=b;”?

java工具类调用问题。 代码如下 为啥运行结构还是 a=2,b=5. 没有进行交换?

python 交换两个变量的值

python中变量的交换

Python交换两个变量值的函数

python之小技巧积累