Python使用逗号交换值会导致混淆

4
这涉及到我在尝试解决链表反转问题时遇到的一个问题。
首先,让我放一些预备代码来定义链表和快速生成链表的方法:
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

    def __repr__(self):
        if self.next:
            return "{}->{}".format(self.val, repr(self.next))
        else:
            return "{}".format(self.val)

def genNode(*nodes, end=None):
    if len(nodes) == 1 and type(nodes[0]) == list:
        nodes = nodes[0]
    for i in nodes[::-1]:
        n = ListNode(i)
        n.next, end = end, n
    return n if nodes else None

我的问题是,我发现交换机制仍然取决于我编写的变量序列。

原本在Python中讨论交换值时,我们可以这样做:

a, b = b, a

如果我有相同的要求,它应该以相同的方式工作。

b, a = a, b

我会尝试写一个反转链表的方法,有三个变量交换,这个想法很简单,创建一个虚拟头节点,并不断在虚拟头节点和虚拟头节点的下一个节点之间添加节点,以便将其反转。
def rev(head):
    dummy = ListNode('X')
    while head:
        dummy.next, head.next, head = head, dummy.next, head.next
    return dummy.next

a = genNode(1,2,3,4)
print(rev(a)) # >>> 4->3->2->1

但是,如果我稍微调整一下这3个变量的顺序:

def rev2(head):
    dummy = ListNode('X')
    while head:
        dummy.next, head, head.next, = head, head.next, dummy.next,
    return dummy.next

a = genNode(1,2,3,4)
print(rev2(a))  # >>> AttributeError: 'NoneType' object has no attribute 'next'

看起来这里是序列很重要,有人可以告诉我如果有超过两个变量,Python如何评估交换值吗。

谢谢!


Python的哪个版本? - Anthony Kong
它是Python 3.6.4。 - jxie0755
真的,不要那样做。a, b = b, a 对于简单的交换是可以的,但对于更复杂的情况,最好编写单独的赋值语句。 - chepner
你要交换的数据类型不是简单的基本数据类型,而是涉及到其他对象链接的类对象,这可能会导致意外的行为! - Devesh Kumar Singh
@chepner 我打算从现在开始这样做。我不知道这种情况可能发生。一直以为所有东西都是同时评估的。 - jxie0755
4个回答

5

从左到右

查看https://docs.python.org/3/reference/simple_stmts.html#assignment-statements

CPython实现细节:在当前实现中,目标的语法被视为与表达式相同,并且在代码生成阶段拒绝无效的语法,从而导致错误消息不够详细。

虽然赋值的定义意味着左侧和右侧之间的重叠是“同时发生”的(例如,a,b=b,a交换了两个变量),但在分配给变量的集合内部出现的重叠是从左到右进行的,有时会导致混淆。例如,以下程序将打印[0,2]:

x = [0, 1]
i = 0
i, x[i] = 1, 2         # i is updated, then x[i] is updated
print(x)

将其与 OP 的示例联系起来:在 rev() 中,headhead.next 被赋值之后更改;在 rev2 中,head 在更改之前更改,从而改变了被分配的内容。 - Scott Hunter
这很有趣,如果我尝试 x[i], i = 2, 1 它实际上会起作用。因此,顺序很重要,但可能会令人困惑。 - jxie0755
那么,(C)Python遍历右侧的值元组并将它们逐个按顺序分配给左侧的名称元组,因此是顺序执行的?如果您将逗号分隔的列表解释为元组,则这种操作方式似乎相当合理。 - Jan Christoph Terasa
那么,您是否建议我回到传统的交换方法,通过创建临时变量来确保安全,当涉及到复杂/多个交换情况时? - jxie0755
1
与不更改正在迭代的list类似,您可能应该避免分配给可以影响同一赋值语句中的其他赋值的名称,就像在示例中一样。该示例是非常构造的,我从未见过这样的代码。 - Jan Christoph Terasa
正如您在更简单的示例中看到的那样,对列表应用交换逻辑并不是非常一致的,因此旧的临时变量是可行的方式。 - Devesh Kumar Singh

1
以下是一个简单的例子,展示了在像 ListNode 这样的类中使用交换的注意事项。
让我们定义一个由3个元素组成的链表。
a = ListNode(1)
b = ListNode(2)
c = ListNode(3)
a.next = b
b.next = c
print(a)
#1->2->3

现在,如果我们交换b和c,它不会产生任何影响。
b,c = c,b
print(a)
#1->2->3

如果我们交换a和b,链表将会改变。
a,b=b,a
print(a)
#2->3

同样地,将a和c交换。
a,c=c,a
print(a)
#3

所以你可以看到,使用简单的交换逻辑在应用于ListNode时是不一致的,因此应该避免使用。

谢谢,这是一个很好的演示。我应该避免在复杂的数据结构中使用交换方法。 - jxie0755
很好,请考虑给这个示例点赞,如果它对您有帮助的话,因为您已经接受了另一个答案! - Devesh Kumar Singh

0

有趣的讨论,有点扩展了上面的答案,我创建了下面这个新的例子。

x = [1, 0]
i = 0
i, x[i] = x[i], i
print(i, x)
>> 1 [1, 0]

让我们逐步了解一下i,x [i] = x [i],i的情况。

  1. 最初,所有变量都处于上一个阶段,即i = 0,因此两侧的x [i]都是x [0] = 1。我们有0、x [0] = x [0]、0或0、1 = 1、0
  2. 交换/赋值从左到右开始。逗号的左部分i = x [i]首先发生,即i = 1i的值从0变为1。
  3. 重要的是,在逗号的右部分发生时,i的值已经改变。实际上,我们正在看1,x [1] = 1,0,令人困惑的是右侧的i不会改变,其值仍为0,而不是新值1,x [1] = 1,i。因此,最终状态是1,x [1] = 1,0

-1

如果有两个以上的变量,它的工作方式与两个变量相同。您可以将它们放在所需的最终顺序中:

>>> a = 1
>>> b = 2
>>> c = 3
>>> c,b,a = a,b,c
>>> a,b,c
(3, 2, 1)

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