Python浅拷贝和深拷贝
浅拷贝(shallow copy)和深度拷贝(deep copy)
1.=
、==
、is
、[:]
的区别
=
是赋值操作符,将左侧变量指向右侧变量的内存地址==
只能用于判断两个变量的值是否相等,不能用于赋值,也不比较内存地址is
判断两种是否指向同一个内存地址[:]
是切片操作符,用于复制列表、元组、字符串等序列类型,返回一个浅拷贝的副本
2.浅拷贝和深拷贝的区别
- 浅拷贝:指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。
- 深拷贝:所谓深度拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
例一:
a = [1, 2, [3, 4]]
b = a #让b指向a指向的地方,也就是a和b是同一个东西
print(a is b) #True
print(a == b) #True
c = list(a) #浅拷贝
print(a is c) #False
print(a == c) #True
d = a[:] #浅拷贝
print(a is d) #False
print(a == d) #True
#但是对于不可改变的数据类型
m = (1, 2, [3, 4])
e = tuple(m)
print(m is e) #True
print(m == e) #True
到这里,对于浅拷贝你应该很清楚了。浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。因此,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会带来一些副作用,尤其需要注意。我们来看下面的例子:
例二:
a = [[1, 2], (30, 40)]
b = list(a)
a.append(100) #这个操作不会对 b 产生影响,因为 a 和 b 作为整体是两个不同的对象,并不共享内存地址
a[0].append(3)#因为 b 是 a 的浅拷贝,b 中的第一个元素和 a 中的第一个元素,共同指向同一个列表,因此 b 中的第一个列表也会相对应的新增元素 3。
print(a) #[[1, 2, 3], (30, 40), 100]
print(b) #[[1, 2, 3], (30, 40)]
#这里就很清晰了,对于拷贝列表里面的元素,是共享一个内存地址,但是列表整体不是一个内存地址
#浅拷贝里的元素是对原对象元素的引用,因此 b 中的元素和 a 指向同一个列表和元组对象
a[1] += (50, 60) #因为元组是不可变的,这里表示对 a 中的元组拼接,然后重新创建了一个新元组作为 a 中的第二个元素,而 b 中没有引用新元组,因此 b 并不受影响。
print(a) #[[1, 2, 3], (30, 40, 50, 60),100]
print(b) #[[1, 2, 3], (30, 40)]
通过这个例子,你可以很清楚地看到使用浅拷贝可能带来的副作用。因此,如果我们想避免这种副作用,完整地拷贝一个对象,你就得使用深度拷贝。
例三:copy和deepcopy函数
import copy
a = [[1, 2], (30, 40)]
b = copy.copy(a)
print(a is b) #False
print(a == b) #True
#Python 中以 copy.deepcopy() 来实现对象的深度拷贝
c = copy.deepcopy(a) #False
print(a is c) #True
a.append(100)
print(b) #[[1, 2], (30, 40)]
print(c) #[[1, 2], (30, 40)]
a[0].append(3)
print(b) #[[1, 2, 3], (30, 40)]
print(c) #[[1, 2], (30, 40)]
可以看到无论 a 怎么变化,c 始终不受影响,因为它们完全独立,没有联系。
不过,深度拷贝也不是完美的,往往也会带来一系列问题。如果被拷贝对象中存在指向自身的引用,那么程序很容易陷入无限循环:
import copy
x = [1]
x.append(x)
y = copy.deepcopy(x)
print(x) #[1, [...]]
print(y) #[1, [...]]
上面这个例子,列表 x 中有指向自身的引用,因此 x 是一个无限嵌套的列表。但是我们发现深度拷贝 x 到 y 后,程序并没有出现 stack overflow 的现象。这是为什么呢?
其实,这是因为深度拷贝函数 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,我们来看相对应的源码就能明白:
def deepcopy(x, memo=None, _nil=[]):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
if memo is None:
memo = {}
d = id(x) # 查询被拷贝对象 x 的 id
y = memo.get(d, _nil) # 查询字典里是否已经存储了该对象
if y is not _nil:
return y # 如果字典里已经存储了将要拷贝的对象,则直接返回
...
一些需要注意的地方
通常来说,在实际工作中,当我们比较变量时,使用’==‘的次数会比’is’多得多,因为我们一般更关心两个变量的值,而不是它们内部的存储地址。但是,当我们比较一个变量与一个单例(singleton)时,通常会使用’is’。一个典型的例子,就是检查一个变量是否为 None:
if a is None:
...
if a is not None:
...
这里注意,比较操作符is
的速度效率,通常要优于==
。因为is
操作符不能被重载,这样,Python 就不需要去寻找,程序中是否有其他地方重载了比较操作符,并去调用。执行比较操作符is
,就仅仅是比较两个变量的 ID 而已。
但是==
操作符却不同,执行a == b
相当于是去执行a.__eq__(b)
,而 Python 大部分的数据类型都会去重载__eq__这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__函数会去遍历列表中的元素,比较它们的顺序和值是否相等。