参考:
Python 的深拷贝和浅拷贝
Python的变量与对象(不可变对象与可变对象)
0. 前提
对于不可变对象,无论深、浅拷贝,内存地址 (id) 都是一成不变的;
对于可变对象,则存在 3 种不同情况:直接赋值、浅拷贝、深拷贝
不可变对象与可变对象的知识点可参考:Python的变量与对象(不可变对象与可变对象)
1. 直接赋值
直接赋值:仅拷贝了对可变对象的引用,故前后变量均未隔离,任一变量 / 对象改变,则所有引用了同一可变对象的变量都作相同改变。例如:
>>> x = [555, 666, [555, 666]]
>>> y = x # 直接赋值, 变量前后并未隔离
>>> y
[555, 666, [555, 666]]
# 修改变量 x, 变量 y 也随之改变
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777]
>>> y
[555, 666, [555, 666], 777]
# 修改变量 y, 变量 x 也随之改变
>>> y.pop()
777
>>> y
[555, 666, [555, 666]]
>>> x
[555, 666, [555, 666]]
在某些情况下,这是致命的,因此还需要深、浅拷贝来正确实现真正所需的拷贝目的。
2. 浅拷贝
浅拷贝:使用 copy(x) 函数,拷贝可变对象如 list 的最外层对象并实现隔离,但 list 内部的嵌套对象仍是未被隔离的引用关系。例如:
>>> import copy
>>> x = [555, 666, [555, 666]]
>>> z = copy.copy(x) # 浅拷贝
>>> zz = x[:] # 也是浅拷贝, 等同于使用 copy() 函数的 z
>>> z
[555, 666, [555, 666]]
>>> zz
[555, 666, [555, 666]]
# 改变变量 x 的外围元素, 不会改变浅拷贝变量
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777] # 只有自身改变, 增加了外围元素 777
>>> z
[555, 666, [555, 666]] # 未改变
>>> zz
[555, 666, [555, 666]] # 未改变
# 改变变量 x 的内层元素, 则会改变浅拷贝变量
>>> x[2].append(888)
>>> x
[555, 666, [555, 666, 888], 777] # 同时发生改变, 增加了内层元素 888
>>> z
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888
>>> zz
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888
# 浅拷贝变量的外围元素改变不会相互影响
>>> z.pop(0)
555
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> z
[666, [555, 666, 888]] # 只有自身改变, 弹出了外围元素 555
>>> zz
[555, 666, [555, 666, 888]] # 未改变
# 浅拷贝变量的内层元素改变会相互影响
>>> z[1].pop()
888
>>> x
[555, 666, [555, 666], 777] # 同时发生改变, 弹出了内层元素 888
>>> z
[666, [555, 666]] # 同时发生改变, 弹出了内层元素 888
>>> zz
[555, 666, [555, 666]] # 同时发生改变, 弹出了内层元素 888
注意,所谓改变应包含 “增、删、改” 三种,以上仅展示了前两种情况,第三种不言自明。
此外,若有人问元组 (tuple) 一定是不可变的吗?答案是不一定,因为浅拷贝时仅隔离最外层对象,而内层嵌套对象则仍为引用关系,例如:
>>> t = (1, 2, [3, 4]) # tuple
>>> import copy
>>> ct = copy.copy(t) # 浅拷贝 tuple # 注意, 令 ct = t 时此例结果仍然相同
>>> ct
(1, 2, [3, 4])
>>> ct[2][-1] = 5 # 修改 ct
>>> ct
(1, 2, [3, 5])
>>> t # t 也随之改变, 证明内层嵌套对象仍为引用关系
(1, 2, [3, 5])
3. 深拷贝
深拷贝:使用 deepcopy(x[,memo]) 函数,拷贝可变对象如 list 的“外围+内层”而非引用,实现对前后变量不论深浅层的完全隔离。例如:
>>> import copy
>>> x = [555, 666, [555, 666]]
>>> k = copy.deepcopy(x) # 深拷贝
>>> k
[555, 666, [555, 666]]
# 改变变量 x 的外围元素, 不会改变深拷贝变量
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777]
>>> k
[555, 666, [555, 666]] # 未改变
# 改变变量 x 的内层元素, 同样不会改变深拷贝变量
>>> x[2].append(888)
>>> x
[555, 666, [555, 666, 888], 777]
>>> k
[555, 666, [555, 666]] # 未改变
# 深拷贝变量的外围元素改变不会相互影响
>>> k.pop(0)
555
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> k
[666, [555, 666]]
# 深拷贝变量的内层元素改变同样不会相互影响
>>> k[1].pop()
666
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> k
[666, [555]]
再次试验元组 (tuple) 的例子以展示浅拷贝和深拷贝的区别与联系:
>>> t = (1, 2, [3, 4]) # tuple
>>> import copy
>>> ct = copy.deepcopy(t) # 深拷贝 tuple
>>> ct
(1, 2, [3, 4])
>>> ct[2][-1] = 5 # ct 改变
>>> ct
(1, 2, [3, 5])
>>> t # t 不论外层还是内层嵌套变量, 均不变 (完全隔离)
(1, 2, [3, 4])
4. 代码示例
4.1 列表浅拷贝与深拷贝
import copy
a = [1,2,3,[4,5,6]]
b = a # 直接赋值: 引用对象,原始对象的父对象及子对象均被引用
c = a.copy() # 浅拷贝:深拷贝父对象(一级目录),子对象(二级目录)不拷贝,还是引用
d = copy.deepcopy(a) # 深拷贝,包含对象里面的自对象的拷贝,所以原始对象的改变不会造成深拷贝里任何子元素的改变
a.append('AA')
a[3].append('BB')
print("a:", a)
print("b:", b)
print("c:", c)
print("d:", d)
输出:
a: [1, 2, 3, [4, 5, 6, 'BB'], 'AA']
b: [1, 2, 3, [4, 5, 6, 'BB'], 'AA']
c: [1, 2, 3, [4, 5, 6, 'BB']]
d: [1, 2, 3, [4, 5, 6]]
4.2 字典浅拷贝与深拷贝
import copy
dict1 = {'user': 'A',
'num': [1, 2, 3]}
dict2 = dict1 # 直接赋值: 引用对象,原始对象的父对象及子对象均被引用
dict3 = dict1.copy() # 浅拷贝:深拷贝父对象(一级目录),子对象(二级目录)不拷贝,还是引用
dict4 = copy.deepcopy(dict1) # 深拷贝
dict1['user'] = 'BBB'
dict1['num'].remove(1)
# 输出结果
print("dict1: ", dict1)
print("dict2: ", dict2)
print("dict3: ", dict3)
print("dict4: ", dict4)
输出:
dict1: {'user': 'BBB', 'num': [2, 3]}
dict2: {'user': 'BBB', 'num': [2, 3]}
dict3: {'user': 'A', 'num': [2, 3]}
dict4: {'user': 'A', 'num': [1, 2, 3]}