Python中的浅拷贝

4

我对浅拷贝的工作原理有些困惑,我的理解是当我们执行 new_obj = copy.copy(mutable_obj) 时,会创建一个新对象,其中的元素仍指向旧对象。

以下是我感到困惑的例子 -

## assignment
i = [1, 2, 3]
j = i
id(i[0]) == id (j[0])  # True
i[0] = 10
i  # [10, 2, 3]
j  # [10, 2, 3]

## shallow copy
k = copy.copy(i)
k   # [10, 2, 3]
id(i) == id(k)  # False (as these are two separate objects)
id(i[0]) == id (k[0])  # True (as the reference the same location, right?)
i[0] = 100
id(i[0]) == id (k[0])  # False (why did that value in that loc change?)
id(i[:]) == id (k[:])  # True  (why is this still true if an element just changed?)
i   # [100, 2, 3]
k   # [10, 2, 3]

在浅拷贝中,k[0] 不就像赋值一样指向 i[0] 吗?那么当 i[0] 改变时,k[0] 不应该也跟着改变吗?
我为什么期望它们相同呢?因为 -
i = [1, 2, [3]]
k = copy(i)
i  # [1, 2, [3]]
k  # [1, 2, [3]]
i[2].append(4)
i  # [1, 2, [3, 4]]
k  # [1, 2, [3, 4]]
id(i[0]) == id (k[0])  # True
id(i[2]) == id (k[2])  # True
id(i[:]) == id (k[:])  # True

1
int 是不可变对象,因此您需要在 i[0] 上设置一个新对象。 - Willem Van Onsem
1
为什么你认为 i[0] 仍然指向 k[0]?这两个列表是不同的,因此一个列表中的更新不会反映到另一个列表中。 - Willem Van Onsem
3
只要没有嵌套数组,浅拷贝和深拷贝是相同的。 - Pika Supports Ukraine
1
@SirGoPythonJavaCppRubythe3rd 哦,那很有道理。 你能解释一下为什么在浅拷贝示例中 id(i[:]) == id (k[:]) 为True(即使k有一个新元素)吗? - Ani Menon
1
id(i[:]) == id(k[:]) # True(如果一个元素刚刚改变,为什么这仍然是真的?)。这可能是CPython实现的细节。对于列表来说,id基于内存地址,这个地址很可能被重复使用,因为i[:]立即被垃圾回收了。这两个列表不是同一个对象。 - Håken Lid
显示剩余8条评论
3个回答

3

id(i) == id(k) # False (因为它们是两个不同的对象)

正确。

id(i[0]) == id (k[0]) # True (因为它们引用了相同的位置,对吗?)

正确。

i[0] = 100

id(i[0]) == id (k[0]) # False (为什么那个位置上的值会改变?)

它改变了,因为你在前一行中将它更改了i[0] 曾经指向10,但现在你已经将它指向100。因此,i[0]k[0]现在不再指向同一个位置。

指针(引用)是单向的10 不知道谁在指向它, 100也一样。它们只是内存中的位置。因此,如果你改变了i的第一个元素指向的位置,k并不关心(因为ki不是同一个引用)。 k的第一个元素仍然指向它一直指向的那个位置。

id(i[:]) == id (k[:]) # True (如果一个元素刚刚被改变,为什么这仍然是真的?)

这个问题比较微妙,请注意:

>>> id([1,2,3,4,5]) == id([1,2,3])
True

相比之下

>>> x = [1,2,3,4,5]
>>> y = [1,2,3]
>>> id(x) == id(y)
False

它涉及垃圾回收和id的某些微妙之处,详细解释在这里。简而言之,当您说id([1,2,3,4,5]) == id([1,2,3])时,首先我们创建[1,2,3,4,5],然后通过调用id获取它在内存中的位置。然而,[1,2,3,4,5]是匿名的,因此垃圾收集器立即将其回收。然后,我们创建另一个匿名对象[1,2,3],CPython决定将其放在刚清理的位置。 [1,2,3]也被立即删除和清除。但如果您存储引用,则GC无法干扰,然后引用将不同。

可变对象示例

如果重新分配可变对象,则会发生相同的情况。以下是示例:

>>> import copy
>>> a = [ [1,2,3], [4,5,6], [7,8,9] ]
>>> b = copy.copy(a)
>>> a[0].append(123)
>>> b[0]
[1, 2, 3, 123]
>>> a
[[1, 2, 3, 123], [4, 5, 6], [7, 8, 9]]
>>> b
[[1, 2, 3, 123], [4, 5, 6], [7, 8, 9]]
>>> a[0] = [123]
>>> b[0]
[1, 2, 3, 123]
>>> a
[[123], [4, 5, 6], [7, 8, 9]]
>>> b
[[1, 2, 3, 123], [4, 5, 6], [7, 8, 9]]

当你执行 a[0].append(123) 时,我们修改了 a[0] 所指向的对象。恰好此时 b[0] 也指向同一个对象(即 a[0]b[0] 是指向同一对象的引用)。
但如果你将 a[0] 指向一个新的对象(通过赋值操作,例如:a[0] = [123]),那么 b[0]a[0] 就不再指向同一位置。

@AniMenon 很好,很高兴能够帮助! - Matt Messersmith

2
在Python中,所有的东西都是对象,包括整数。所有的列表都只持有对象的引用。替换列表中的一个元素并不意味着元素本身会发生改变。
考虑另一个例子:
class MyInt:
    def __init__(self, v):
        self.v = v
    def __repr__(self):
        return str(self.v)

>>> i = [MyInt(1), MyInt(2), MyInt(3)]
[1, 2, 3]
>>> j = i[:] # This achieves the same as copy.copy(i)

[1, 2, 3]
>>> j[0].v = 7
>>> j
[7, 2, 3]
>>> i
[7, 2, 3]

>>> i[0] = MyInt(1)
>>> i
[1, 2, 3]
>>> j
[7, 2, 3]

我在这里创建了一个MyInt类,它只包含一个整数。 通过修改类的实例,两个列表都发生了“改变”。但是,由于我替换了一个列表条目,这两个列表现在不同了。
对于整数也是一样的。你只能读取它们,不能修改它们。

0
  • 在第一种情况下,j = i 是一个赋值语句,j 和 i 都指向同一个列表对象。
    当你改变列表对象的元素并打印 i 和 j 时,由于 i 和 j 都指向同一个列表对象,而且已经改变的是元素而不是列表对象本身,所以它们都会打印相同的输出。
  • 在第二种情况下,k = copy.copy(i) 是一个浅拷贝,其中复制了列表对象和嵌套引用的副本,但内部不可变对象没有被复制。
    浅拷贝不会创建嵌套对象的副本,而只是复制嵌套对象的引用。请参考这个 https://www.programiz.com/python-programming/shallow-deep-copy
  • 因此,i 和 k 有不同的引用集合,指向相同的不可变对象。当你执行 i[0] = 100 时,列表 i 中的引用指向一个新的 int 对象,其值为 100,但 k 中的引用仍然指向值为 10 的旧 int 对象。

@Ani Menon 欢迎。 - shirish
在浅拷贝中,不可变对象被复制,但对于所有可变对象,它们的引用被复制。不正确的是浅拷贝对可变和不可变对象执行相同的操作。 - juanpa.arrivillaga
@juanpa.arrivillaga 请审查 - shirish

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