0%

Python - 浅拷贝 shallow copy 和深拷贝 deep copy 的区别到底是什么?


浅谈两者理论区别与实操避雷。


不同的 copy 方式与内存地址的变化

用不同的方式构建新的列表,然后查看他们各自的内存地址:

import copy

list1 = [[1, 2], (30, 40)] # 原始列表
list2 = list1 # 使用 = 创建列表
list3 = list(list1) # 使用数据类型本身的构造器进行浅拷贝
list4 = copy.copy(list1) # 使用 copy.copy() 进行浅拷贝
list5 = copy.deepcopy(list1) # 使用 copy.deepcopy() 进行深拷贝
list6 = list1[0] # 截取部分

print("id list1: ", id(list1))
>> id list1: 4431556800

print("id list2: ", id(list2))
>> id list2: 4431556800 # 内存地址不变

print("id list3: ", id(list3))
>> id list3: 4431510080 # 使用数据类型本身的构造器进行浅拷贝:内存地址变了

print("id list4: ", id(list4))
>> id list4: 4431550528 # 浅拷贝:内存地址变了

print("id list5: ", id(list5))
>> id list5: 4431559680 # 深拷贝:内存地址变了

print("id list6: ", id(list6))
>> id list6: 4431557248 # 截取部分:内存地址变了

新列表中各自元素的内存地址?

如果你对浅拷贝有一定的了解,你也许会知道浅拷贝虽然会给新的列表重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。基于此,我们继续查看列表中元素的内存地址,看是否和理论一致。

print("id list1[0]: ", id(list1[0]))
>> id list1[0]: 4431557248

print("id list4[0]: ", id(list4[0]))
>> id list4[0]: 4431557248 # 浅拷贝:元素地址一致

print("id list5[0]: ", id(list5[0]))
>> id list5[0]: 4431569344 # 深拷贝:元素地址也不一样

我们会发现,浅拷贝的元素地址和原对象保持一致,且正好是我们上面创建的 list6 的内存地址!


浅拷贝 (Shallow Copy) 可能引发的问题

会受到影响的情况

基于上面的结论和发现,我们不难想到,当我们向 list6 添加新的元素时,list1 / list2 / list3 / list4 都会发生变化,因为他们都包含了这个元素,而 list5 则不会受到影响:

list6.append(3)

print(list6)
>> [1, 2, 3]

print(list1, list2, list3, list4)
>> [[1, 2, 3], (30, 40)], [[1, 2, 3], (30, 40)], [[1, 2, 3], (30, 40)], [[1, 2, 3], (30, 40)] # 随之变化

print(list5)
>> [[1, 2], (30, 40)] # 不变

或者是我们更改 list6 中的元素时:

list6[0] = 99

print(list6)
>> [99, 2, 3]

print(list1, list2, list3, list4)
>> [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] # 随之变化

print(list5)
>> [[1, 2], (30, 40)] # 不变

不会受到影响的情况

但值得注意的是,我们进行上面的操作时,本质上没有更新 list6 的内存地址,而其他的列表引用了这个内存地址,所以才导致了变化。

但是,如果我们的操作也会改变 list6 的内存地址呢?

print("id list6: ", id(list6))
>> id list6: 4431557248

list6 = 66
print("id list6: ", id(list6))
>> id list6: 4391690640 # 注意 list6 的内存地址更新了

print(list1, list2, list3, list4)
>> [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] [[99, 2, 3], (30, 40)] # 没有变化

所以,如果使用了浅拷贝,在修改任何相关元素时,最好是弄清楚该元素是否被其他对象引用,以及内存地址在不同的操作中是否发生了变化。尤其是在处理 list、dict、set、自定义类型等可变对象时,更需要谨慎。


深拷贝 (deep copy)

当然,如果要追求绝对稳妥又不担心内存占用,那么深拷贝会是一个很好的应对方法。深拷贝会为新对象重新分配一块内存,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联

不过,深度拷贝也不是完美的:如果被拷贝对象中存在指向自身的引用,那么程序就会陷入无限循环…


一些补充点

对于 Python 中的不可变对象,比如intfloatboolstrtuple ,不管是浅拷贝还是深拷贝,都是引用而不会创建新的。

tuple1 = (1, 2, 3)
tuple2 = copy.copy(tuple1)
tuple3 = copy.deepcopy(tuple1)

print("tuple1 is tuple2 ?", tuple1 is tuple2)
print("tuple1 is tuple3 ?", tuple1 is tuple3)

>> tuple1 is tuple2 ? True
>> tuple1 is tuple3 ? True

进而我们可以想象,如果我们修改了 list1 中的元组:因为元组是不可变的,这里表示对 list1 中的第二个元组拼接,然后重新创建了一个新元组作为 list1 中的第二个元素,而浅拷贝的 list3 / list4 中没有引用新元组,因此并不受影响。

list1[1] += (50, 60)

print(list1)
>> [[99, 2, 3], (30, 40, 50, 60)]

print(list3)
>> [[99, 2, 3], (30, 40)]

而对于 pandas.DataFrame 而言:

# 浅拷贝
df_2 = df_1

# 深拷贝
df_2 = copy.copy(df_1)
df_2 = df_1.copy()