后端开发 \ Python \ Python--浅拷贝和深拷贝

Python--浅拷贝和深拷贝

总点击88
简介:之前,我在博文Python–内存管理中说明了Python中对象赋值的问题,我们已经知道,当创建一个对象,并且把这个对象赋值给另一个变量的时候,其实并没有拷贝这个对象,而只是给这个对象增加了一个引用(这一点具体可以参见链接给出的博文中“引用计数”这一节)

之前,我在博文Python–内存管理中说明了Python中对象赋值的问题,我们已经知道,当创建一个对象,并且把这个对象赋值给另一个变量的时候,其实并没有拷贝这个对象,而只是给这个对象增加了一个引用(这一点具体可以参见链接给出的博文中“引用计数”这一节)

赋值是引用而非拷贝

复习一下,比如下面的例子

a = 2

b = a

print(id(a),id(b)) # >>> 4443297952 4443297952

# 对于列表这种可变类型,也是一样的

c = [1,2,"we"]

d = c

print(id(c),id(d)) # >>> 4362693768 4362693768

正因为没有真正意义上拷贝,所以“一变俱变”

c = [1,"we"]

d = c

d[1] = 3

print(d) # >>> [1,3,"we"]

print(c) # >>> [1,"we"]

浅拷贝

1. 三种拷贝方法

但是这显然是不能满足满足实际要求的,所以,我们需要相应的拷贝方法,就以列表为例,拷贝方法有三种

# 切片[:]

a = ["we",1,2]

b = a[:]

b[0] = "you"

print(b,id(b)) # >>> ['you',2] 4427664520

print(a,id(a)) # >>> ['we',2] 4427664648

# copy()函数

a1 = ["we",2]

b1 = a1.copy()

b1[1] = 3

print(b1,id(b1)) # >>> ['we',2] 4537855112

print(a1,id(a1)) # >>> ['we',2] 4537855240

# 工厂函数,比如列表的list(),字典的dict()等

a2 = ["we",2]

b2 = list(a2)

b2[2] = 3

print(b2,id(b2)) # >>> ['we',3] 4507116488

print(a2,id(a2)) # >>> ['we',2] 4507081864

从以上三个例子可以看出,三种拷贝方法都达到了相同的效果,拷贝生成了一个新的对象,对新对象做更改,将不影响原对象。但是,需要注意的是,我们这个例子里面列表的元素可都是原子型的(字符串,数字等等),如果是可变类型的,情况就不一定了。

2. 拷贝对象,但未拷贝对象的内容

还是上面的例子,让我们再深入一步

a = [1,3]

b = a.copy()

print([id(x) for x in a]) # >>> [4412074112,4412074144,4412074176]

print([id(x) for x in b]) # >>> [4412074112,4412074176]

不难发现,实际上b虽然是a的拷贝,但是b的内容却全都是a的内容的引用,并未拷贝。

我们将这种新建了对象,但是对象的内容q是却是原对象的引用的拷贝,称之为浅拷贝。

但是,如果我们重新对新对象的元素更改时,如果这个元素是原子型的,比如数字,字符串,元组。那么其实是新建了个对象,然后令这个别名,引用这个新对象。所以,看上去好像是真的对对象的内容也拷贝了似的,但其实没有。话说的有点绕,看下面这个例子,也许会清楚些。

a = [1,3]

b = a.copy()

# 根据上面的例子可知,此时a与b的每个元素的id一定是一样的

print([id(x) for x in a]) # >>> [4339624064,4339624096,4339624128]

# 新建了对象(整数0),并令b[0]这个名字引用这个对象

b[0] = 0

# 此时,b[0]的地址就变了

print([id(x) for x in b]) # >>> [4339624032,4339624128]

# 但是,a却没有改变

print([id(x) for x in a]) # >>> [4339624064,4339624128]

我想,上面的讲解已经说明了为什么类似于切片,copy() 函数,工厂函数这些拷贝方法看上去确实是新建了对象内容(因为改变拷贝对象的内容,原对象未受影响),但是他的机制依然是浅拷贝,拷贝对象的内容依然是原对象相应内容的引用。

知道这个有什么用呢?比如当d中对象包含列表这种情况,我们就不会认为对拷贝对象的列表元素的改变会保证原对象不变。

a = ['I',[1,2]]

b = a.copy()

b[1][0] = 0

print(b) # >>> ['I',[0,2]]

# 这里特别小心,b的内容变了,而且因为b的内容只是对a中相应内容的引用,所以a也变了。

print(a) # >>> ['I',2]]

深拷贝

那么,当然会遇到需要在拷贝对象的同时,也将对象的所有内容拷贝的场景。也就是说一种从内而外,从小到大的全部拷贝。我们将这种“彻底”的拷贝,称为“深拷贝”。

深拷贝的方法也简单,引入copy库,用到 copy.deepcopy() 函数即可

import copy

a = ['I',2]]

b = copy.deepcopy(a)

print([id(x) for x in a]) # >>> [4380781288,4381566152]

print([id(x) for x in b]) # >>> [4380781288,4381565384]

需要注意的是,对于对象的内容是原子型的,比如上面例子中,列表的第一个元素是字符串,则不存在深拷贝的问题,一概使用浅拷贝。

其实,这当然是完全不会影响我们应用的,因为原子型的对象,我们进行修改时,会重新新建对象,然后引用,这个上面已经说过了。比如

import copy

a = ['I',2]]

b = copy.deepcopy(a)

b[0] = "You"

b[1][1] = 3

# 随意改变b,a完全不受影响

print(b) # >>> ['You',3]]

print(a) # >>> ['I',2]]

意见反馈 常见问题 官方微信 返回顶部