阅读本文大概需要 8 分钟
前言
面试的时候,深拷贝和浅拷贝是经常会被问到的问题,如果是你去面试,被问到它们之间有什么区别,你会怎么回答?
变量赋值
在理解深拷贝和浅拷贝之前,首先得知道一个问题:变量赋值。
在之前的文章里,介绍到 Python 中的变量其实是对象的引用,也就是变量只是指向了对象在内存中的所在地址。
如常见的:
>>> origin = [1,2,3,[4,5]]
>>> fz = origin
>>> origin == fz # == 比较的是对象本身
True
>>> origin is fz # is 比较的是对象所在地址
True
>>> id(origin)
1492148448
>>> id(fz)
1492148448
可以发现它们的地址是一样的,这里是因为变量origin
只是对象[1,2,3,[4,5]]]
的一个引用,相当于给它贴了一个标签,而fz = origin
则是相当于给对象[1,2,3,[4,5]]
又贴了一层标签,origin
和fz
指向的都是对象[1,2,3,[4,5]]
的地址。
当对象本身发生变化时,不管对象引用是什么,由于他们都是指向对象,仅仅只是一个标签,故也会随之发生变化。
>>> a = [1,2,3]
>>> b = a
>>> a == b
True
>>> a is b
True
>>> a.append(4)
>>> b
[1, 2, 3, 4]
浅谈深拷贝和浅拷贝
无论在什么时候,对一些重要数据进行备份是一件很必要的事情,而怎么对数据进行拷贝,则成了所有语言中的一个重要知识点。
用到拷贝的时候需要导入 copy 模块
浅拷贝
>>> import copy
>>> origin = [1,2,3,[4,5]]
>>> cop = copy.copy(origin)
>>> cop
[1, 2, 3, [4, 5]]
>>> id(origin)
42761672
>>> id(cop)
42760584
>>> origin.append(6)
>>> origin
[1, 2, 3, [4, 5], 6]
>>> cop
[1, 2, 3, [4, 5]]
可以看到经过浅拷贝处理后,变量origin
和cop
所指向的对象的地址,已经不是同一个地址,说明浅拷贝已经单独开辟了一个内存空间,里面存放的是跟origin
一样的内容,且在对origin
进行append
追加处理后,cop
并没有发生变化,说明这两个已经是不同的对象,从而实现了对原对象的拷贝,并「独立」于原对象。
可是真的做到完全「独立」吗?
>>> origin[3].append(6)
>>> origin
[1, 2, 3, [4, 5, 6], 6]
>>> cop
[1, 2, 3, [4, 5, 6]]
可以看到,在对origin
里的嵌套列表进行append
追加操作时,进过浅拷贝后的cop
里的嵌套列表也发生了变化,这是为什么呢?
原来所谓的「浅拷贝」只是对目标进行一层拷贝,如果目标里还嵌套了其他的对象,浅拷贝只是将它的引用进行拷贝,并没有将其指向的对象进行拷贝,所以当其发生变化时,浅拷贝后的对象自然也发生变化。
深拷贝
懂了浅拷贝就很容易理解深拷贝。
import copy
origin = [1,2,3,[4,5]]
dcop = copy.depcopy(origin)
>>> id(origin)
42698888
>>> id(dcop)
42698952
>>> origin.append(6)
>>> origin[3].append(6)
>>> dcop
[1, 2, 3,[4,5]]
可以发现深拷贝是单独开辟了一个内存空间,并且直接复制的对象内容,当原先被复制的对象发生改变时,并没有影响到经过深拷贝的对象,因为它们是分开的两个对象,互相并不影响。深拷贝实现了真正独立。
赋值和拷贝的区别
赋值和拷贝在某些时候很相似,那他俩到底有什么区别呢?
>>> import copy
>>> origin = [1,2,3,[4,5]]
>>> fz = origin
>>> cop = copy.copy(origin)
>>> dcop = copy.deepcop(origin)
>>> fz == cop
True
>>> fz is cop
False
>>> fz == dcop
True
>>> fz is dcop
False
>>> id(origin)
42760392
>>> id(fz)
42760392
>>> id(cop)
42761672
>>> id(dcop)
42760584
>>> origin[3][1] = 'Python'
>>> origin
[1, 2, 3, [4, 'Python']]
>>> fz
[1, 2, 3, [4, 'Python']]
>>> cop
[1, 2, 3, [4, 'Python']]
>>> dcop
[1, 2, 3, [4, 5]]
赋值就好比一个电脑小白去电脑城里卖电脑,一些无良商家就会乘机宰你一顿,你去跟商家说,我要一台华硕的电脑,商家立马把电脑包装一撕,递给你。这时候你想到来之前在网上看的帖子,买电脑要验机!
你说不行,我怎么就能相信这就是华硕电脑呢?你得把电脑拆了,我检查检查,然后商家就把电脑拆了,让你看。你看着贴在零件上面一个个的标签,嗯~买对了,等你痛痛快快交完钱,回家上网一测,却发现里面的主板被换了,显卡也不是原装的了。。。
而拷贝就相当于是一个老手去买电脑,他一进电脑城,点名就要 mac,显卡要 GeForce2 MX,音响要 Odyssey,显示器要 Apple Studio Display。。。。老板知道你不好忽悠,也拿一台 mac 给你,也当场拆机。
copy 就像是一个混迹电脑吧多年的 14 级大佬,理论大于实践,只知道一些比较出名的型号是什么,却不知道里面装的究竟是阿猫还是阿狗,也就只能拆一层看看,再往下就看不懂了。
而 deepcopy 就像是一个电脑专家,亲自验机,一个一个撕开标签,看看标签下面的到底是什么,最后发现,嗯~是正品,然后跟老板来句,你这出厂日期有点远了啊,打个 8 折吧。。。。
也就是说赋值只是单纯的贴个新标签,而 copy 只能撕开了一层标签,但 deepcopy 是一层一层往下撕标签,直到最后一层。
埋在浅拷贝里面的坑
说到浅拷贝的时候,提到经过浅拷贝的处理的,是单独创建一个新的内存空间,里面存放着拷贝过来的对象,但真的是这样吗?
>>> import copy
>>> origin = (1,2,3,[4,5])
>>> cop = copy.copy(origin)
>>> dcop = copy.deepcopy(origin)
>>> id(origin)
38303208
>>> id(cop)
38303208
>>> id(dcop)
38845320
这里经过浅拷贝处理后cop
,它的地址居然跟原对象是一样的!
原来浅拷贝在处理时,会根据要拷贝的对象类型进行不同处理,当对象类型是固定类型时,浅拷贝并不会开辟一个新的内存空间,而只是对原对象的一个引用,跟赋值操作是一样的,而当对象类型是可变类型时,则会开辟一个新的内存空间。
而深拷贝则不会有这种情况,无论原对象是什么类型,都是开辟一个新的内存空间!
结语
深拷贝和浅拷贝是一个很重要的知识点,简单的说:
浅拷贝会根据原对象的类型决定是否开辟一个新的内存空间,且若原对象是复杂结构,如列表里面嵌套列表,元组里面嵌套列表,则只会拷贝第一层对象,再往下只会拷贝对象引用。
深拷贝是直接开辟一个新的内存空间,并且遇到复杂结构会逐层向下,直到最后一层。
您的一次关注就足以让我感动!