Python中的a,b = b,a实现方式是什么?它与C++中的swap函数有何不同?

17

当我想尝试Python版本的以下问题时,我遇到了这个问题: https://leetcode.com/problems/first-missing-positive/discuss/17071/My-short-c++-solution-O(1)-space-and-O(n)-time

我不确定为什么 a[0], a[a[0]] = a[a[0]], a[0] 这行代码没有做交换?

>>> nums
[2, 1, 0]
>>> a = [2,1,0]
>>> a[0], a[a[0]] = a[a[0]], a[0]
>>> a
[2, 1, 0]
>>> a[0]
2
>>> a[0],a[2] = a[2], a[0]
>>> a
[0, 1, 2]

我的猜测是,a,b = b,a语法的实现大概是这样的:

tmp = a[0] (tmp = 2)
a[0]  = a[a[0]] (a[0] = a[2] = 0)
a[a[0]] = tmp (a[a[0]] = a[0] = tmp = 2)

然后我查看了C++中swap函数的实现。我对C++一无所知,但是看起来思路是一样的: http://www.cplusplus.com/reference/algorithm/swap/

The behavior of these function templates is equivalent to:
template <class T> void swap (T& a, T& b)
{
  T c(std::move(a)); a=std::move(b); b=std::move(c);
}
template <class T, size_t N> void swap (T (&a)[N], T (&b)[N])
{
  for (size_t i = 0; i<N; ++i) swap (a[i],b[i]);
}

我们有 c = a,然后 a = b,b = a。那么为什么 C++ 的 swap 函数没有这个问题呢?如何用 Pythonic 的方式编写这种 swap 函数?

1
如果您使用不同的列表(例如 a = [1,2,3,4]),您会发现它确实更改了值,但问题在于执行顺序,因此 a[a[0]] 在两次调用时指向不同的元素。 - Zinki
可能是 Python 中的多重赋值和求值顺序 的重复问题。 - agtoever
有趣的事实:如果你使用 a = [2,3,4],你会得到“列表分配索引超出范围”的错误。 - molbdnilo
我认为这个问题与C++中的“序列点”问题有些相关,因为你的左侧既修改又使用了a[0] - molbdnilo
5个回答

10
这种行为确实与Python评估类型表达式的方式有关。
a,b=b,a

实际上,Python 的操作是首先通过创建元组 (b,a) 来“准备”右侧的值。然后,这个元组被解包并按相反顺序分配给变量。
需要注意的是,虽然 Python 使用引用指向对象,但是如果这些变量名引用的对象是不可变类型的值,则变量名所指向的对象可能会更改。对于可变类型却不是这样(在 Python FAQ 中有示例进行说明)。
针对您使用的可变类型(列表)的示例进行分解:
a = [2,1,0]    
a[0], a[a[0]] = a[a[0]], a[0]
  1. a[a[0]]从列表a中取出值为0的元素(等于2),作为索引再次从列表a中取出值为2的元素。
  2. a[0]2,因此创建的元组为(0,2)
  3. 元组(0,2)被解包,0替换列表中的2(第0个元素)。
  4. 现在,a[a[0]]可以理解为:取列表a的第0个元素(当前为0),然后将元组解包中的2替换该位置处的列表值(现在02替换 - 这使得操作看起来对列表没有任何影响)。

如建议所述,在来自 von Oak 的答案中更改顺序会有所帮助,因为上面第4点的步骤不会再次替换值。

我建议您参考传递赋值答案了解函数和参数传递。


需要注意的是,临时元组是使用变量的值(实际上是值的副本),而不是对变量的引用创建的(您可以在此处阅读有关按值传递和按引用传递之间差异的内容)。这完全不正确。Python 从不 使用引用调用,并且这与此处无关,因为这涉及到函数参数的工作原理。此外,在Python中没有“引用”类型。所有对象的语义完全相同,而不管类型如何。 - juanpa.arrivillaga
我认为你可能想要谈论的是关于C++“赋值语义”(而不是像按引用传递与按值传递这样的评估策略,这些完全是不同的主题)的“引用 vs 值语义”。Python仅支持相当于C++引用语义的内容,即赋值永远不会复制。 - juanpa.arrivillaga
@juanpa.arrivillaga:谢谢你。你是正确的。起初,我使用了一些令人困惑的术语。现在已经通过参考Python的相关FAQ进行了更正。 - sophros

6
要理解这个,您需要通过dis深入了解实现。
首先,让我们考虑一个简单的交换函数:
from dis import dis

def swap(i, j):
    i, j = j, i

dis(swap)

输出字节码:

4             0 LOAD_FAST                1 (j)
              2 LOAD_FAST                0 (i)
              4 ROT_TWO
              6 STORE_FAST               0 (i)
              8 STORE_FAST               1 (j)
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

您可以看到这里有一个ROT_TWO,它的意思是:

交换栈顶的两个元素。

ROT_TWO主要负责交换操作。

现在回答您的问题:

我们来看一个正在工作的示例:

from dis import dis

def swap():
    a = [2, 1]
    a[0], a[1] = a[1], a[0]

dis(swap)

输出字节码

  4           0 LOAD_CONST               1 (2)
              2 LOAD_CONST               2 (1)
              4 BUILD_LIST               2
              6 STORE_FAST               0 (a)

  5           8 LOAD_FAST                0 (a)
             10 LOAD_CONST               2 (1)
             12 BINARY_SUBSCR
             14 LOAD_FAST                0 (a)
             16 LOAD_CONST               3 (0)
             18 BINARY_SUBSCR
             20 ROT_TWO
             22 LOAD_FAST                0 (a)
             24 LOAD_CONST               3 (0)
             26 STORE_SUBSCR
             28 LOAD_FAST                0 (a)
             30 LOAD_CONST               2 (1)
             32 STORE_SUBSCR
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

输出字节码与简单的交换函数时类似的。但是当代码发生改变时

from dis import dis

def swap():
    a = [1, 0]
    a[0], a[a[0]] = a[a[0]], a[0]
dis(swap)

swap()

输出结果为:

  4           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (0)
              4 BUILD_LIST               2
              6 STORE_FAST               0 (a)

  5           8 LOAD_FAST                0 (a)
             10 LOAD_FAST                0 (a)
             12 LOAD_CONST               2 (0)
             14 BINARY_SUBSCR
             16 BINARY_SUBSCR
             18 LOAD_FAST                0 (a)
             20 LOAD_CONST               2 (0)
             22 BINARY_SUBSCR
             24 ROT_TWO
             26 LOAD_FAST                0 (a)
             28 LOAD_CONST               2 (0)
             30 STORE_SUBSCR
             32 LOAD_FAST                0 (a)
             34 LOAD_FAST                0 (a)
             36 LOAD_CONST               2 (0)
             38 BINARY_SUBSCR
             40 STORE_SUBSCR
             42 LOAD_CONST               0 (None)
             44 RETURN_VALUE

您可以看到输出的字节码,其中前两个项目相同。因此它不会交换


我认为“交换两个堆栈中最顶部的项”仍然不是一个实现细节。这两个项如何交换? - Sraw
@Sraw,请检查我的更新说明。我认为现在会更有意义。 - Arghya Saha

2

这个问题可以很容易地在纸上(例如在面试时)考虑,您不需要调试或将代码反汇编为字节码才能理解。

我认为这与C ++中swap函数的实现无关。这些是不相关的事情。

你只需要知道右侧完全先被评估,然后从左到右按顺序将表达式右侧的值分配给左侧的值。 Sophros回答得很正确,我只是进一步扩展了这个想法并详细说明了它。

想象第一个例子。 我们有:

a = [2,1,0]

a[0], a[a[0]] = a[a[0]], a[0]

当我们开始执行这段代码时,右侧先进行评估,因此我们会得到
a[0], a[a[0]] = a[a[0]], a[0]    # a[a[0]] == 0, a[0] == 2, a == [2, 1, 0]

在右侧,我们有元组(0, 2),而a仍然是[2, 1, 0]。接下来,我们从左边开始对表达式进行赋值,所以对于a[0],我们将第一个项目从元组中分配给它,即0。现在我们有:
a[0], a[a[0]] = (0, 2)   # a[0] == 0, a == [0, 1, 0]

现在我们执行任务的最后一部分,即将a[a[0]]赋值为2。但是a[0]现在是0,所以在减少之后,我们将a[0]的值赋为2。因此,在最后一个赋值之后,值如下:

a[0], a[a[0]] = (0, 2)   # a[a[0]] == 2, a == [2, 1, 0]

似乎没有任何改变,值也没有交换,但从上面可以看出,a[2,1,0],然后是[0,1,0],最后又是[2,1,0]。因此,似乎什么都没有改变,交换不起作用。
现在是第二种情况,我们只改变表达式中变量的顺序:
a = [2,1,0]

a[a[0]], a[0] = a[0], a[a[0]]

当我们开始执行这段代码时,右侧先进行评估,因此我们会得到

a[a[0]], a[0] = a[0], a[a[0]]    # a[0] == 2, a[a[0]] == 0, a == [2, 1, 0]

在右边,我们有元组(2, 0),而a仍为[2, 1, 0]

接下来,我们从左侧开始逐个分配,将元组的第一个项目 2 分配给表达式的左侧的 a[a[0]]a[0]2,所以分配后,我们将值 2 分配给 a[2]。现在我们有:

a[a[0]], a[0] = (2, 0)   # a[a[0]] == 2, a == [2, 1, 2]

现在我们执行任务的最后一部分,即将a[0]赋值为0。因此,最后一次赋值后的值为

a[a[0]], a[0] = (2, 0)   # a[0] == 0, a == [0, 1, 2]

现在这个代码按照预期工作。
因此,在交换表达式中存在依赖变量时,也需要考虑顺序。所谓的依赖变量是指在第一种情况下,左侧有a[0],a[a[0]],这意味着a[0]改变了它的值,而a[a[0]]使用了这个改变后的值,这会导致不想要的行为。
最后,无论使用哪种编程语言,最好不要使用依赖变量(例如将数组索引用于另一个数组索引),当您想要交换它们的值时。

交换 a[a[a[0]]]a[a[0]] 怎么样?会起作用吗? - n. m.
我大大扩展了我的回答,因此现在您可以将您的示例作为练习尝试:-) - von Oak

2

Python和C ++是不同的语言,有不同的规则。这就是为什么表面上相似的结构在这些语言中行为不同的主要原因。

你不能在Python中编写一个通用的swap,它可以处理像a [0],a [a [0]]这样的输入。这不是问题。您永远不应该尝试在任何语言中交换这样的内容,以避免混淆并提高作为程序员的未来就业机会。

如果您绝对需要交换由同一数组的元素索引的数组元素,则可以在Python中这样做:

p, q, a[p], a[q] = index0, index1, a[q], a[p]

其中index0index1可以是任何涉及a[i]a[a[i]]a[a[a[i]]]或类似内容的表达式。例如:

p, q,  a[p], a[q] = a[0], a[a[0]],  a[q], a[p]

工作。


0

这实际上是 Python 的一个特性,称为多重赋值,如果你来自其他语言,可能有些难以理解。它的工作原理如下。

例如:a, b = b, a

这段代码实际上会交换元素。我将给出一个简单直观的解释,然后再给出一个更技术性的解释。

  1. 首先计算 RHS,然后将值分别分配给 LHS 上的变量(标签)。
  2. 因此,在 Python 中,您实际上可以将元组 (a,b) 定义为 a,b,括号只是为了更好的可读性。因此,以下所有代码片段都是等效的。
a, b = b, a
a, b = (b, a)
a, b = [b, a]

注意:在Python中,变量的概念与其他语言非常不同,它们是用于存储该类型值的容器。在Python中,“标签”是比“变量”更正确的术语,因为您只是使用此名称标记对象,并且该对象可以是任何数据类型。因此,当您执行a,b = b,a时,您实际上并没有交换值,而是交换了标签。
因此,Python首先查找RHS上指向标签ba的值,将这些值放在那里,然后仅为这些值提供新标签。


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