Python 列表切片陷阱:引用、拷贝与深拷贝
Python 列表的切片和赋值操作很基础,之前也遇到过一些坑,以为自己很懂了。但今天刷 Codewars 时发现了一个更大的坑,故在此记录。
Python 列表赋值:拷贝了“值”还是“引用”?
很多入门 Python 的人会犯这样一个错误:在赋值操作=
中搞不清是赋了“值”还是“引用”。比如:
a = [1, 2, 3]
b = a
b[0] = 10 # 更改列表 b 的第一个元素,但 a 现在也被更改为了 [10, 2, 3]
他可能只想改变列表b
,但实际上这样也会改变列表a
。
因为b
实际上是列表a
的另一个引用,a
和b
是同一个对象,id(a) == id(b)
,所以更改b
也会更改a
。这个应该大部分人都知道。所以正确的代码应该使用切片来进行列表的拷贝:
a = [1, 2, 3]
b = a[:] # 使用切片进行列表拷贝
b[0] = 10 # 此时 a 和 b 是两个不同的对象
二维列表引发的思考:列表的本质
好的,现在我们确定切片能够进行列表的拷贝。那我们就能心安理得地改动新的列表了吗?请看二维列表(二维数组):
a = [[1, 2, 3], [4, 5, 6]]
b = a[:]
b[0][0] = 10
此时,a
还是被改动了!
原因是,虽然id(a) == id(b)
为False
,a
和b
确实不是同一个对象。但它们的元素都是同一个对象——id(a[0]) == id(b[0])
,id(a[1]) == id(b[1])
。因为列表里存储的是对象的引用!
列表 list 终究只是个容器。就像 tuple 本身是 immutable (不可变)的,但它只是容器,它可以存储一个可变对象,因此呈现出一种可以被改动的“假象”。例如:
>>> a = ([1],)
>>> a[0][0] = 2
>>> a
([2],)
所以容器和它存储的对象不能混为一谈。所以对于这种二维列表,想要进行完全的拷贝,请直接使用copy.deepcopy()
深度拷贝。
如果只想拷贝一部分(切片),那可以先拷贝再切片:
>>> import copy
>>> a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> b = copy.deepcopy(a)[1:]
>>> b[0][0] = 100
>>> a
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> b
[[100, 5, 6], [7, 8, 9]]
此时修改b
没有影响到a
。