【问题标题】:Why can flipping a tuple assignment change its behavior?为什么翻转元组赋值可以改变它的行为?
【发布时间】:2018-09-23 00:44:10
【问题描述】:

一直以来,我一直认为a, b, c = c, a, ba, c, b = c, b, a 相同...我认为这是一种同时分配变量的方法,因此您不必创建一堆临时变量。但显然它们是不同的,因为有人破坏了我的代码。

这是我的原始/工作实现:

class Node:
    def __init__(self, v = None, next = None):
        self.v = v
        self.next = next
    def __repr__(self):
        return "Node(v=%r, nextV=%r)" % (self.v, self.next.v if self.next else None)

a = Node(1)
b = Node(2)
a.next = b

def flip(nodeA, nodeB):
    nodeB, nodeA.next, nodeA = nodeA, nodeB, nodeA.next
    return (nodeA, nodeB)

a, b = flip(a, b)
print "A=%r; B=%r" % (a, b)

其正确/预期的行为是交换链表中的两个节点,如下面的输出所示:

A=Node(v=2, nextV=None); B=Node(v=1, nextV=2)

但是,如果我像这样重新排序翻转功能:

def flip(nodeA, nodeB):
    nodeB, nodeA, nodeA.next = nodeA, nodeA.next, nodeB
    return (nodeA, nodeB)

...那个输出坏了:

A=Node(v=2, nextV=2); B=Node(v=1, nextV=2)

节点 A 最终得到一个指向自身的指针(它的 nextVv 相同),因此沿着这棵树的尝试将永远递归。

为什么这些结果不一样?元组解包不应该表现得好像所有分配都同时发生吗?

【问题讨论】:

  • @CharlesDuffy,开心吗?
  • 我给出了一个关于 python 如何从左到右(不是同时)分配事物的答案,但是看到代码我不确定问题出在分配而不是它的含义,这就是封装一个无限递归循环
  • @Raksha 请尊重。
  • @Raksha 正如我所说,保持尊重。叫人巨魔不会帮助你得到答案。 minimal reproducible example 是必需的。在您提供问题后,我将尝试回答您的问题。我还将撤回我的反对票并结束投票。
  • 顺便说一句,如果我尝试编辑问题以获得正确的 MCVE,您会感到被冒犯吗?

标签: python variable-assignment


【解决方案1】:

因为您正在修改的项目之一是另一个项目的属性,所以它们不是相互独立的——需要一个序列化顺序来确定操作将做什么,并且该操作是从左到右的.

让我们看看它是如何发挥作用的,通过编写这段代码,就像使用临时变量一样。


鉴于以下共享前奏:

old_nodeA      = nodeA
old_nodeB      = nodeB
old_nodeA_next = nodeA.next

工作代码类似于以下内容:

# nodeB, nodeA.next, nodeA = nodeA, nodeB, nodeA.next

nodeB      = old_nodeA
nodeA.next = old_nodeB       # nodeA is still the same as old_nodeA here
nodeA      = old_nodeA_next

这是损坏的:

# nodeB, nodeA, nodeA.next = nodeA, nodeA.next, nodeB

nodeB      = old_nodeA
nodeA      = old_nodeA_next
nodeA.next = old_nodeB       # we're changing old_nodeA_next.next, not old_nodeA.next

区别在于nodeA.next指的是两种情况之间不同nodeAnext属性。


让我们看看在运行时一切正常的情况下这是如何工作的,使用一些伪代码显示对象 ID,以便您可以区分对象是就地变异还是引用改变:

# Working implementation
###############################################################
# id(nodeA) # id(nodeB) # AAA.v # AAA.next # BBB.v # BBB.next # 
###############################################################
# AAA       # BBB       # 1     # BBB      # 2     # None     # Starting condition
# AAA       # AAA       # 1     # BBB      # 2     # None     # nodeB = old_nodeA
# AAA       # AAA       # 1     # BBB      # 2     # None     # nodeA.next = old_nodeB
# BBB       # AAA       # 1     # BBB      # 2     # None     # nodeA = old_nodeA_next

在工作场景中,我们将AB的名称切换为各自指对端的节点;没有其他任何改变。

相比之下:

# Broken implementation
###############################################################
# id(nodeA) # id(nodeB) # AAA.v # AAA.next # BBB.v # BBB.next # 
###############################################################
# AAA       # BBB       # 1     # BBB      # 2     # None     # Starting condition
# AAA       # AAA       # 1     # BBB      # 2     # None     # nodeB = old_nodeA
# BBB       # AAA       # 1     # BBB      # 2     # None     # nodeA = old_nodeA_next
# BBB       # AAA       # 1     # BBB      # 2     # BBB      # nodeA.next = old_nodeB

当我们到达 nodeA.next = old_nodeB 时,名称 nodeA 已经被分配了最初与节点 B 关联的 id(在我们的示例中为 BBB),因此我们更改了 original 节点 B 的next指向自身的指针,在问题的核心生成循环。

【讨论】:

  • 我不明白这是什么意思....什么“不同”c?这个“不同”的c没有下一个吗?它究竟在什么时候破裂?
  • 我添加的 cmets 有帮助吗?
  • 不是真的......这是= 创建指针而不是像列表一样创建副本的事情之一吗?所以old_c_next 不仅仅是一个节点,即使它是一个新变量……我只是想把整个过程可视化
  • 请记住,nodeA.next 是一个不同的变量,具体取决于 nodeA 是什么。因此,如果您在更改nodeA.next 之前更改nodeA,则您正在修改具有next 属性的对象,该属性是分配目标。
  • @Raksha,顺便说一句,我的表中有一个错误来自最初组装的损坏版本;查看更正。
猜你喜欢
  • 2018-01-13
  • 2018-05-08
  • 1970-01-01
  • 1970-01-01
  • 2012-04-03
  • 1970-01-01
相关资源
最近更新 更多