Python中的对象引用浅拷贝与深拷贝

Posted To be a better programmer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中的对象引用浅拷贝与深拷贝相关的知识,希望对你有一定的参考价值。

最近项目中遇到一个Python浅拷贝机制引起的bug,由于对于Python中对象引用、赋值、浅拷贝/深拷贝机制没有足够的认识,导致调试了很久才发现问题,这里简单记录一下相关概念。

在Python的设计哲学中,Python中的每一个东西都是对象,都有一个ob_refcnt变量,这个变量维护着对对象的引用计数,决定着对象的创建与消亡。

所以在Python程序中,一般的赋值操作其实只是将左值指向了右值的引用,并不会创建新的对象,可以通过id函数查看Python中对象在内存中的唯一标识,以list对象为例,如下代码:

>>> alist=[[1,2],3,4]
>>> blist=alist
>>> id(alist);id(blist) #alist/blist实际引用内存中的同一个list对象
140357688098184
140357688098184
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4, 5] #由于实际引用同一个list对象,blist增加一个元素后,alist的取值实际上是完全一样的
>>> id(alist);id(blist)
140357688098184
140357688098184

 

在上面的代码中,将alist的值赋给blist,其实只是把blist指向了alist在内存中的对象,两者引用了同一个list对象,此时如果对blist append一个新元素,由于是指向同一个对象,alist的输出结果一样会变化。

通过slice语法或者copy模块的copy函数,可以实现浅拷贝--

>>> import copy
>>> alist=[[1,2],3,4]
>>> blist=alist[:]
>>> clist=copy.copy(alist)
>>> id(alist);id(blist);id(clist) #alist/blist/clist实际已经指向内存中的不同list对象
140357691858696
140357691897864
140357720939912
>>> id(alist[0]);id(blist[0]);id(clist[0]) #alist[0]/blist[0]/clist[0]三个子对象依然指向内存中的同一个list对象
140357691897800
140357691897800
140357691897800
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4] #blist对象值的变更,不会再影响到alist和clist
>>> clist
[[1, 2], 3, 4]
>>> alist[0].append(a)
>>> alist
[[1, 2, a], 3, 4]
>>> blist
[[1, 2, a], 3, 4, 5] #由于实际引用同一对象,alist[0]子对象值的变更,也会从blist[0]/clist[0]上体现出来
>>> clist
[[1, 2, a], 3, 4]
>>> id(alist[1]);id(blist[1]);id(clist[1])
10919488
10919488
10919488

 

可以看到blist和clist本身已经是新的list对象,不再引用alist这个list对象,但是三个list中的子对象还是相同的引用,因为python中的浅拷贝只能拷贝父对象,不会拷贝对象内部的子对象。

通过copy模块中的copy.deepcopy函数可以实现深拷贝:

>>> alist=[[1,2],3,4]
>>> blist=copy.deepcopy(alist)
>>> id(alist);id(blist) #alist/blist已经引用内存中不同的list对象
140357692023560
140357691897608
>>> blist.append(5)
>>> blist
[[1, 2], 3, 4, 5]
>>> alist
[[1, 2], 3, 4] #blist取值的变更,不会影响到alist
>>> id(alist[0]);id(blist[0]) #alist{0]/blist[0]两个子对象也已经引用内存中不同的list对象
140357691897864
140357691896136
>>> alist[0].append(a)
>>> alist
[[1, 2, a], 3, 4]
>>> blist
[[1, 2], 3, 4, 5] # alist[0]子对象值的变更,也不会再印象到blist[0]的值
>>> id(alist[1]);id(blist[1])
10919488
10919488

 

可以看到,通过copy.deepcopy进行拷贝后,alist和blist指向不同的list对象,同时其子对象alist[0]/blist[0]也指向了不同的list对象,但是alist[1]/blist[1]还是指向相同的对象,这是因为3、4在Python中其实是不可变对象,相当于是常量,在Python中不可变对象只会存在唯一一份,所以无论浅拷贝/深拷贝,都是对同一个不可变对象进行的引用。

对于dict/set这些Python类型对象的赋值操作,也会存在类似的浅拷贝/深拷贝的问题,下面再以dict为例贴一下代码:

引用赋值:

>>> adct={d:{1:2}, 3:4}
>>> bdct=adct
>>> id(adct);id(bdct) #adct/bdct实际引用内存中的同一个dict对象
140357688090760
140357688090760
>>> id(adct[d]);id(bdct[d]) #adct[‘d‘]/bdct[‘d‘]两个子对象实际引用内存中的同一个dict对象
140357691897928
140357691897928
>>> bdct[d].update({a:b})
>>> bdct
{d: {1: 2, a: b}, 3: 4}
>>> adct
{d: {1: 2, a: b}, 3: 4} #由于实际指向同一个子对象,bdct[‘d‘]取值的变更会直接影响到adct的值

copy.copy浅拷贝:

>>> adct={d:{1:2}, 3:4}
>>> bdct=copy.copy(adct)
>>> id(adct);id(bdct) #adct/bdct引用不同的dict对象
140357688082888
140357720937544
>>> id(adct[d]);id(bdct[d]) #adct[‘d‘]/bdct[‘d‘]两个子对象依然指向内存中同一个dict对象
140357688101704
140357688101704
>>> bdct[d].update({a:b})
>>> bdct
{d: {1: 2, a: b}, 3: 4}
>>> adct
{d: {1: 2, a: b}, 3: 4} #由于实际引用同一个子对象,bdct[‘d‘]子对象值的变更会直接影响到adct的值

copy.deepcopy深拷贝:

>>> adct={d:{1:2}, 3:4}
>>> bdct=copy.deepcopy(adct)
>>> id(adct);id(bdct) #adct/bdct本身已经引用不同的dict对象
140357691897928
140357688094152
>>> id(adct[d]);id(bdct[d]) #adct/bdct的子对象引用了不同的dict子对象
140357688090760
140357688085896
>>> bdct[d].update({a:b})
>>> bdct
{d: {1: 2, a: b}, 3: 4}
>>> adct
{d: {1: 2}, 3: 4} #bdct[‘d‘]子对象的变更不会再影响到adct[‘d‘]的值

 

以上是关于Python中的对象引用浅拷贝与深拷贝的主要内容,如果未能解决你的问题,请参考以下文章

Python中的浅拷贝与深拷贝

Python中的赋值(复制)浅拷贝与深拷贝

python直接赋值浅拷贝与深拷贝的区别解析

Python基础对象的浅拷贝与深拷贝

Python基础对象的浅拷贝与深拷贝

Python基础对象的浅拷贝与深拷贝