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

时间:2021-06-27 15:36:29

标签: python python-3.x list indexing swap

众所周知,交换 ab 两项值的 Pythonic 方法是

a, b = b, a

它应该等价于

b, a = a, b

然而,今天我在做一些代码的时候,无意中发现以下两个交换给出了不同的结果:

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

7 个答案:

答案 0 :(得分:73)

来自python.org

<块引用>

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

...

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

所以我认为这意味着你的任务

df2.TestCaseName = pd.Categorical(df2.TestCaseName,
                                  categories=df1.TestCaseName.values,
                                  ordered=True)

df2 = df2.sort_values('TestCaseName')

大致相当于

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

答案 1 :(得分:51)

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

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

答案 2 :(得分:13)

这是按照规则发生的:

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

因此,对于 JFileChooser f = new JFileChooser("D:\\"); f.showOpenDialog(null); FileNameExtensionFilter fnef = new FileNameExtensionFilter("IMAGES", "png", "jpg", "jpeg"); f.setFileFilter(fnef); File selected = f.getSelectedFile(); String selectedPath = selected.getAbsolutePath(); Image getAbsolutePath = null; ImageIcon icon = new ImageIcon(selectedPath); Image image = icon.getImage(); selectedPath = selectedPath.replace("\\","\\\\"); // INSERT IN MYSQL String sql = "INSERT INTO stage1 (ATTESTATION) VALUES (" + selectedPath + ")"; pst = (PreparedStatement) cnx.prepareStatement(sql); pst.executeUpdate(sql); JOptionPane.showMessageDialog(null, "succes!!"); } catch (SQLException ex) { ex.printStackTrace(); } ,您的代码在第一种情况下

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]

答案 3 :(得分: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

当运行结果:

b get 2
a get 1
a set 2
b set 1

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

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

答案 4 :(得分:5)

在表达式的左侧,您同时在读取和写入 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 :(得分:4)

在 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 部分。)问题的重要部分是 store 的计算是在单独和独立的批次中按顺序完成的。

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

答案 6 :(得分:1)

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

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

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