python 中的浅拷贝

shallow copy in python

我对浅拷贝的工作原理有点困惑,我的理解是当我们这样做时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

在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]

我正在这里创建一个 class MyInt,它只包含一个 int。 通过修改 class 的一个实例,两个列表 "change"。但是,当我替换列表条目时,列表现在不同了。

整数也是如此。你只是不能修改它们。

  • 第一种情况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中的引用指向一个新的值为100的int对象,但是k中的引用仍然引用旧的值为10的int对象。

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?)

它改变了,因为你在上一行改变了它i[0] 指向 10,但您将其更改为指向 100。因此,i[0]k[0] 现在不再指向同一个点。

指针(引用)是单向10不知道指向它的是什么。 100 也没有。它们只是内存中的位置。因此,如果您更改 i 的第一个元素指向的位置,k 不在乎(因为 ki 不是相同的引用)。 k 的第一个元素仍然指向它一直指向的位置。

id(i[:]) == id (k[:]) # True (why is this still true if an element just changed?)

这个有点微妙,但请注意:

>>> 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的一些细微之处,在此深入解答:Unnamed Python objects have the same 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] 指向一个 new 对象(通过赋值,如 a[0] = [123]),那么 b[0]a[0]不再指向同一个地方。