关于python:切片操作是否给我一个深或浅的副本?

Does a slicing operation give me a deep or shallow copy?

官方的python文档说,使用切片操作符并在python中进行赋值,可以得到切片列表的一个很浅的副本。

但是当我写代码时,例如:

1
2
o = [1, 2, 4, 5]
p = o[:]

当我写下:

1
2
id(o)
id(p)

我得到了不同的ID,并且附加一个列表也不会反映在另一个列表中。它不是在创建一个深层次的副本,还是在某个地方我出错了?


您正在创建一个浅副本,因为嵌套值不会被复制,只会被引用。深度复制也将创建列表引用的值的副本。

演示:

1
2
3
4
5
6
7
8
9
10
>>> lst = [{}]
>>> lst_copy = lst[:]
>>> lst_copy[0]['foo'] = 'bar'
>>> lst_copy.append(42)
>>> lst
[{'foo': 'bar'}]
>>> id(lst) == id(lst_copy)
False
>>> id(lst[0]) == id(lst_copy[0])
True

这里嵌套字典不被复制;它只被两个列表引用。新元素42不共享。

记住,Python中的所有内容都是一个对象,名称和列表元素只是对这些对象的引用。列表的副本创建一个新的外部列表,但新列表只接收对完全相同对象的引用。

适当的深度复制会递归地为列表中包含的每个对象创建新的副本:

1
2
3
4
>>> from copy import deepcopy
>>> lst_deepcopy = deepcopy(lst)
>>> id(lst_deepcopy[0]) == id(lst[0])
False


您应该知道,使用isid的测试可能会误导是否使用不可变和内部对象(如字符串、整数和包含不可变的元组)制作真实副本。

考虑一个易于理解的内部字符串示例:

1
2
3
4
5
6
>>> l1=['one']
>>> l2=['one']
>>> l1 is l2
False
>>> l1[0] is l2[0]
True

现在对l1进行一个简单的复制,并测试不可变字符串:

1
2
3
4
5
>>> l3=l1[:]
>>> l3 is l1
False
>>> l3[0] is l1[0]
True

现在复制l1[0]所包含的字符串:

1
2
3
4
5
>>> s1=l1[0][:]
>>> s1
'one'
>>> s1 is l1[0] is l2[0] is l3[0]
True               # they are all the same object

尝试一个deepcopy,其中每个元素都应该被复制:

1
2
3
4
>>> from copy import deepcopy
>>> l4=deepcopy(l1)
>>> l4[0] is l1[0]
True

在每种情况下,字符串'one'都被放入python不可变字符串的内部缓存中,is将显示它们是相同的(它们具有相同的id)。它的实现和版本依赖于什么是被实习的,什么时候实习,所以你不能依赖它。它可以显著提高内存和性能。

你可以强制一个不立即被拘留的例子:

1
2
3
4
5
>>> s2=''.join(c for c in 'one')
>>> s2==l1[0]
True
>>> s2 is l1[0]
False

然后可以使用python intern函数使该字符串引用缓存对象(如果找到):

1
2
3
4
5
>>> l1[0] is s2
False
>>> s2=intern(s2)
>>> l1[0] is s2
True

同样适用于不可变元组:

1
2
3
4
5
6
7
>>> t1=('one','two')
>>> t2=t1[:]
>>> t1 is t2
True
>>> t3=deepcopy(t1)
>>> t3 is t2 is t1
True

不可变列表(如整数)可以将列表成员相互关联:

1
2
3
4
5
6
7
8
>>> li1=[1,2,3]
>>> li2=deepcopy(li1)
>>> li2 == li1
True
>>> li2 is li1
False
>>> li1[0] is li2[0]
True

因此,您可以使用您知道将要复制某些内容的python操作,但最终结果是对实习生不可变对象的另一个引用。is测试仅是在项目可变的情况下对正在制作的副本进行的处理性测试。