了解 Python 中 Class 对象的可变性和多变量赋值

Understanding Mutability and Multiple Variable Assignment to Class Objects in Python

我正在寻找有关可变性和 class 对象的说明。据我了解,Python中的变量是关于为对象分配变量名。

如果该对象是不可变的,那么当我们将两个变量设置为同一个对象时,它将是两个单独的副本(例如 a = b = 3 所以将 a 更改为 4 不会影响 b,因为 3 是一个数字,不可变对象的示例)。

然而,如果一个对象是可变的,那么改变一个变量赋值中的值自然会改变另一个变量中的值(例如 a = b = [] -> a.append(1) 所以现在两个a 和 b 将引用“[1]”)

使用 classes,它似乎比我想象的还要流畅。我在下面写了一个简单的例子来显示差异。第一个 class 是一个典型的节点 class,带有下一个指针和一个值。将两个变量“slow”和“fast”设置为 Node 对象的同一个实例(“head”),然后更改“slow”和“fast”的值不会影响另一个。也就是说,“slow”、“fast”和“head”都指代不同的对象(也通过检查它们的 id() 来验证)。

第二个示例 class 没有下一个指针,只有 self.val 属性。这次改变两个变量之一,“p1”和“p2”,这两个变量都设置为同一个实例,“start”,将影响另一个。尽管“开始”实例中的 self.val 是一个不可变的数字。

'''
The below will have two variable names (slow, fast) assigned to a head Node.
Changing one of them will NOT change the other reference as well.
'''

class Node:

    def __init__(self, x, next=None):
        self.x = x
        self.next = next

    def __str__(self):
        return str(self.x)

n3 = Node(3)
n2 = Node(2, n3)
n1 = Node(1, n2)
head = n1
slow = fast = head
print(f"Printing before moving...{head}, {slow}, {fast}")  # 1, 1, 1
while fast and fast.next:
    fast = fast.next.next
    slow = slow.next
    print(f"Printing after moving...{head}, {slow}, {fast}") # 1, 2, 3
    print(f"Checking the ids of each variable {id(head)}, {id(slow)}, {id(fast)}") # all different

'''
The below will have two variable names (p1, p2) assigned to a start Dummy.
Changing one of them will change the other reference as well.
'''

class Dummy:

    def __init__(self, val):
        self.val = val

    def __str__(self):
        return str(self.val)

start = Dummy(100)
p1 = p2 = start
print(f"Printing before changing {p1}, {p2}")  # 100, 100
p1.val = 42
print(f"Printing after changing {p1}, {p2}")   # 42, 42

这对我来说有点模糊,无法理解幕后实际发生的事情,我正在寻求澄清,因此我可以自信地为同一对象设置多个变量赋值,期望得到一个真实的副本(不求助于"导入副本;copy.deepcopy(x);")

感谢您的帮助

你把事情想得太复杂了。基本的、简单的规则是:每次使用 = 将一个对象分配给一个变量时,您就让变量名引用该对象,仅此而已。对象是否可变没有区别。

使用 a = b = 3,可以使名称 ab 引用对象 3。如果你然后使 a = 4,你使名称 a 引用对象 4,并且名称 b 仍然引用 3.

使用 a = b = [],您已经创建了两个引用同一个列表对象的名称 ab。在执行 a.append(1) 时,您将 1 附加到此列表。您在此过程中没有为 ab 分配任何内容(您没有写任何 a = ...b = ...)。因此,无论您是通过名称 a 还是 b 访问列表,它仍然是您操作的同一个列表。它只能用两个不同的名字来称呼。

在您的 类 示例中也会发生同样的情况:当您编写 fast = fast.next.next 时,您可以使名称快速引用一个新对象。

当您执行 p1.val = 42 时,您并没有使 p1 引用一个新的不同实例,而是更改了该实例的 val 属性。 p1p2 仍然是这个唯一实例的两个名称,因此使用任何一个名称都可以让您引用同一个实例。

这不是不变性与可变性的问题。这是改变对象与重新分配引用的问题。

If that object is immutable then when we set two variables to the same object, it'll be two separate copies

这不是真的。不会制作副本。如果你有:

a = 1
b = a

您有两个对同一对象的引用,而不是该对象的副本。这很好 因为 整数是不可变的。您不能改变 1,因此 ab 指向同一个对象这一事实不会造成任何伤害。

Python 永远不会为您制作隐式副本。如果你想要一个副本,你需要自己明确地复制它(使用 copy.copy,或其他一些方法,如在列表上切片)。如果你这样写:

a = b = some_obj

ab 将指向同一个对象, 不管 some_obj 的类型以及它是否可变。


那么你的例子有什么区别?

在您的第一个 Node 示例中,您实际上从未更改过任何 Node 对象。它们也可能是不可变的。

slow = fast = head

初始赋值使得 slowfast 都指向同一个对象:head。不过在那之后,你会:

fast = fast.next.next

这会重新分配 fast 引用,但实际上不会改变 fast 正在查看的对象。您所做的只是更改 what 对象 fast 引用正在查看。

然而,在你的第二个例子中,你直接改变了对象:

p1.val = 42

虽然这看起来像是重新分配,但事实并非如此。这实际上是:

p1.__setattr__("val", 42)

并且__setattr__改变对象的内部状态。


因此,重新分配会改变正在查看的对象。它将始终采用以下形式:

a = b  # Maybe chained as well.

对比那些看起来像重新分配,但实际上是对对象的变异方法的调用:

l = [0]
l[0] = 5  # Actually l.__setitem__(0, 5)

d = Dummy()
d.val = 42  # Actually d.__setattr__("val", 42)

可变和不可变对象 当程序为运行时,程序中的数据对象存储在计算机的 内存进行处理。虽然其中一些对象可以在该内存中修改 位置,其他数据对象一旦存储在内存中就不能修改。这 属性 是否可以在同一内存位置修改数据对象 它们的存储位置称为可变性。我们可以通过检查对象之前和之前的内存位置来检查对象的可变性 修改后。如果内存位置保持不变,当数据对象是 已修改,这意味着它是可变的。要检查存储数据对象的内存位置,我们使用函数 id()。考虑以下示例

a=[5, 10, 15] 编号(一) #1906292064
[1]=20 编号(一) #1906292064

#给列表a赋值。存储 a 的内存位置的 ID。 #将列表中的第二项 10 替换为新项 20。 #print(a) 使用 print() 函数验证 a 的新值。# 使用函数 #id() 获取 a 的内存位置。 #存放a的内存位置的ID。 内存位置没有改变,因为 ID 仍然存在 (1906292064) 修改变量前后保持不变。这表明列表 是可变的,即它可以在存储它的同一内存位置进行修改