图文代码浅谈Python中Shallow Copy(浅拷贝)和Deep Copy(深拷贝)的区别

前言

以前大概了解过一下<深浅拷贝>的区别,不过没有太深入了解,写这篇文章是因为在学习PyTorch时想搞懂view的含义,而view似乎是跟 浅拷贝(Shallow Copy) 有关的,所以就想先搞懂浅拷贝与 深拷贝(Deep Copy) 的区别先。
这篇文章是根据Python内置的copy模块的官方说明,以及一些在Stack Overflow上的解释,加上自己的一些理解,画图并总结之后写出来。



Shallow Copy vs. Deep Copy

1. 前提

首先说明一下,在Python中 对象(变量)数据 都是保存在内存中的,但是是分开存储的。数据保存在内存中的某个地址中,而对象则保存着数据在内存中的地址,这种对象中记录数据地址的操作叫做引用

2. 官方文档说明(代码非官方)

先上官方说明(自己翻译的版本):
Python中的赋值表达式是不会复制对象的,其操作是将目标和一个对象绑定在一起 。

a = [1, 2, 3]
b = a
print(b == a, id(b) == id(a))
# Out: True True

==== ↓↓ 重点强调 ↓↓ ====

深浅拷贝的区别只适用于 <可变的><包含可变项的> 复合对象 (compound objects,即包含其他对象的对象,如列表list或类实例class instance)。

==== ↑↑ 重点强调 ↑↑ ====

一定要注意、记住、理解上面高亮文字中的划线部分。举些例子,如果对象是不可变的非复合对象(如单个数字、字符串等对象),或者对象是不包含可变项的不可变的复合对象(如元组对象等),那它是不会被执行深拷贝或浅拷贝操作的;如果对象是包含可变项的不可变的复合对象(如元组对象等), 它不会被执行浅拷贝操作,但是可以被执行深拷贝操作;如果对象是可变的或包含可变项的复合对象时,它可以被执行浅拷贝或深拷贝操作。(不能被执行拷贝操作就相当于赋值语句而已)

  • 浅拷贝(Shallow Copy)会构建一个新的复合对象,然后(在可能的范围内)将原复合对象中找到的对象的 引用 插入到新复合对象中,浅拷贝对象与原始对象共享这些对象
  • 深拷贝(Deep Copy)会构建一个新的复合对象,然后将原复合对象中找到的对象 递归地 将其 副本 插入到新复合对象中。
import copy

# 原始对象
a = 1  # 不可变的非复合对象
b = (1, 2, (1, 2, 3))  # 不含可变项的不可变复合对象
c = (1, 2, [1, 2, 3])  # 含可变项的不可变复合对象
d = [1, 2, 3]  # 可变的复合对象
e = [1, 2, (1, 2, 3)]  # 只含不可变项的可变复合对象
f = [1, 2, [1, 2, 3]]  # 含可变项的可变复合对象

# 拷贝(copy函数是浅拷贝,deepcopy函数是深拷贝)
ca = copy.copy(a)
dca = copy.deepcopy(a)
cb = copy.copy(b)
dcb = copy.deepcopy(b)
cc = copy.copy(c)
dcc = copy.deepcopy(c)
cd = copy.copy(d)
dcd = copy.deepcopy(d)
ce = copy.copy(e)
dce = copy.deepcopy(e)
cf = copy.copy(f)
dcf = copy.deepcopy(f)

# 验证 (id不一样,非严格意义上来说可以认为是内存地址不一样,也就可以判断是否执行了拷贝操作)
print(id(ca)==id(a), id(dca)==id(a))  # Out: True True
print(id(cb)==id(b), id(dcb)==id(b))  # Out: True True
print(id(cc)==id(c), id(dcc)==id(c))  # Out: True False
print(id(cd)==id(d), id(dcd)==id(d))  # Out: False False
print(id(ce)==id(e), id(dce)==id(e))  # Out: False False
print(id(cf)==id(f), id(dcf)==id(f))  # Out: False False

## 内部元素验证 (只验证id不一致的内部元素)
print(id(ce[0])==id(e[0]), id(ce[2])==id(e[2]),
	  id(dce[0])==id(e[0]), id(dce[2])==id(e[2]))  # Out: True True True True
print(id(cf[0])==id(f[0]), id(cf[2])==id(f[2]),
	  id(dcf[0])==id(f[0]), id(dcf[2])==id(f[2]))  # Out: True True True Flase
"""
下面这种情况,浅拷贝对象的元素没变,是因为浅拷贝对象cf跟原始对象f是存储在不同内存地址中的,列表中的每个元素在存储列表的内存空间中又是分开存储的
(若用一个大的矩形代表存储列表等复合对象的内存空间,大矩形内部还有各个小矩形,这些小矩形分别代表复合对象的不同元素占据的空间,这些空间内保存着对这些元素对象的引用(指向另一个内存地址),而非其数据),
如果按下面的方式原始(列表)对象,此操作只是对原始对象执行的,而不是对原始对象的元素(对象)执行的操作,若以上面讲的矩形例子作说明,此操作就是将原始对象对应的大矩形A内部的小矩形a位置的对象x换成另一个对象y,
而这并不会影响浅拷贝的对象对应的大矩形B内部的小矩形a位置的对象x,这里说的对象x/y是指内存空间中保存的对该对象的引用而非数据。
因此没有对原始对象的子对象进行直接操作时,浅拷贝对象的子对象是不变的。
"""
f[0] = 666
print(f, cf, dcf)  # Out: [666, 2, [1, 2, 3]] [1, 2, [1, 2, 3]] [1, 2, [1, 2, 3]]
"""
有了上面那段话的解释,下面这种情况应该可以很好理解了,在这种情况中,改变的就不是原始对象了,而是原始对象的子对象,
这个操作是在原始对象的子对象所在内存空间中进行的,而不是在原始对象的内存空间中进行的,
而原始对象与浅拷贝对象共享这些子对象,因此子对象发生改变时,这些改变就会传递给两个父对象,
多说一句,对于两个父对象而言,其内存空间中仍然保存的是对这些子对象的引用,这就很像一个树状图,对于数据的层层引用就是不同的分支流动而已。
"""
f[2][0] = 666
print(f, cf, dcf)  # # Out: [666, 2, [666, 2, 3]] [1, 2, [666, 2, 3]] [1, 2, [1, 2, 3]]	  

能力有限,代码只能给出这些简单的示例,不过加上注释里的解释,应该可以给出大概的思路了,沿着这些思路就可以自己实践感受一下,相信理解会更深刻。简言之,对这些东西的理解,一切都是建立在 <1. 前提> 的基础上的,重点掌握一下引用的概念,和对内存地址/空间的理解,就可以很好地区分深浅拷贝了。

ps:深拷贝中存在而且浅拷贝不会存在的两个问题:

  1. 递归对象(直接或间接包含自身引用的复合对象)可能会导致递归循环;
  2. 由于深层复制会复制所有内容,因此可能会复制了过多的内容,比如打算在副本之间共享的数据。

3. 图解释

只是看文字说明总是无法很好地理解其深层含义的,所以需要 更直观的解释实践操作感受 ,下面给出我自己总结画的一个图(渣渣自己理解的,自我感觉是差不多意思的了),图是什么意思结合本文的说明加自己理解吧:

==》对于下图图中的内容的一些 说明

  1. 颜色:黑色的表示 内存中的数据 ,红色表示 对象 ,深蓝色表示 对象对数据的引用, 深绿色表示 拷贝操作
  2. 线型:直线表示 引用原始内容(对象或数据), 虚线表示 拷贝的副本
  3. 文字:C – (shallow) copy, DC – deep copy, m – mutable, im – immutable, c – compound, o – object, imData – (不可变)数据, 数字 – 编号(用于区分对象)。
    Shallow Copy vs. Deep Copy
    用代码说明一下:
import copy

# 原始复合对象创建
mco1_1 = 0  # Out: 0

mco1 = [mco1_1, 0]  # Out: [0, 0]
mco2 = {0:0}  # Out: {0:0}
imco3 = (0, 0, 0)  # Out: (0, 0, 0)
imData0 = "hello"  # Out: "hello"

mco = [mco1, mco2, imco3, imData0]  # Out: [[0, 0], {0: 0}, (0, 0, 0), 'hello']

# 拷贝
C_mco = copy.copy(mco)  # Out: [[0, 0], {0: 0}, (0, 0, 0), 'hello']
DC_mco = copy.deepcopy(mco)  # Out: [[0, 0], {0: 0}, (0, 0, 0), 'hello']

# 验证第一层
print(id(C_mco)==id(mco), id(DC_mco)==id(mco))  # Out: False False

# 验证第二层
print(id(C_mco[0])==id(mco[0]), id(C_mco[1])==id(mco[1]), 
      id(C_mco[2])==id(mco[2]), id(C_mco[3])==id(mco[3]))  # Out: True True True True
print(id(DC_mco[0])==id(mco[0]), id(DC_mco[1])==id(mco[1]), 
      id(DC_mco[2])==id(mco[2]), id(DC_mco[3])==id(mco[3]))  # Out: False False True True
mco[0] = 666  # 在原始对象的内存空间中,对原始对象进行操作
mco[3] = "wont be copied"  # 在原始对象的内存空间中,对原始对象进行操作
print(mco, C_mco, DC_mco, sep="\n")  # [666, {0: 0}, (0, 0, 0), 'wont be copied']
									 # [[0, 0], {0: 0}, (0, 0, 0), 'hello']
									 # [[0, 0], {0: 0}, (0, 0, 0), 'hello']

# 验证第三层
print(id(C_mco[1][0])==id(mco[1][0]), id(C_mco[2][0])==id(mco[2][0]))  # True True
print(id(DC_mco[1][0])==id(mco[1][0]), id(DC_mco[2][0])==id(mco[2][0]))  # True True
mco[1][0] = 666  # 在原始对象和浅拷贝对象共享的子对象的内存空间中,对共享的子对象进行操作
print(mco, C_mco, DC_mco, sep="\n")  # [666, {0: 666}, (0, 0, 0), 'wont be copied']
									 # [[0, 0], {0: 666}, (0, 0, 0), 'hello']
									 # [[0, 0], {0: 0}, (0, 0, 0), 'hello']  


总结

  1. 深浅拷贝的区别只适用于 <可变的><包含可变项的> 复合对象
  2. 深拷贝最直观的感受是,在新的内存空间中复制了所有的元素,对原始对象(包括其子对象)的任何操作都不会对深拷贝的对象有任何的影响,只要是可变的或包含可变项的复合对象元素,就会在新的内存空间中创建一个副本,其他的依旧是对原始数据的引用;(完全拷贝)
  3. 浅拷贝最直观的感受是,在新的内存空间中复制了原始对象对其所有子对象的引用,它们共享这些子对象,对原始对象的直接改变不会影响到浅拷贝的对象,而对原始对象的子对象的直接/间接改变则会传递给浅拷贝的对象,因为子对象是共享的,可以从内存地址角度去理解


参考文献:

  1. python-copy module
  2. What is the difference between a deep copy and a shallow copy?
  3. Deep copy vs Shallow Copy [duplicate]
  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值