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']的值