[python][进阶之路]理解python中的深复制和浅复制

时间:2022-08-23 13:12:40

简介

在上一篇关于list的复制中的问题中,笔者提到了一些由于list的复制语句产生的问题,如果能够搞明白一切皆对象的py设计思想,那么关于深复制和浅复制也很容易理解。其实这里的深复制和浅复制也类似于cpp概念中的深浅复制。

深复制和浅复制

在上节[3]中提到复制,在python中一切都是对象,每个对象包含了idendity、type 和 value。所以python中的复制语句实际上是添加引用,将内存地址赋予了一个新的别名。

浅复制

浅复制一般出现在非嵌套的对象当中,python中的非容器对象(num,str,其他原子性对象),元组是个例外,后面会讲,对于复制都使用的是浅复制,不分浅复制和深复制。那么对于容器类对象,像list和dict,浅拷贝和深拷贝就有区分了。下面简单的例子:

[python][进阶之路]理解python中的深复制和浅复制

>>> import copy
>>> b = [1, 2]
>>> a = [b, 3, 4]
>>> d = copy.copy(a)
>>> print(id(d) is id(a))
False
>>> print(id(d[0]) is id(a[0]))
True

上述代码中d创建了一个a的浅复制,这时d会新创建一个对象,所以id(a) neq id(b),但是由于a是一个嵌套的容器对象,那么浅复制就不会复制内层内的b的对象,而是复制b的引用。所以id(d[0]) == id(a[0])。这时修改d[0]中的值同样会改变a[0]中的值。

浅复制的几种情况:

  • 使用切片[:]操作
  • 使用工厂函数(如list/dir/set)
  • 使用copy模块中的copy()函数

深复制

深复制相对应与浅复制,浅复制是只有一层的对象复制,其他层都是引用。而深复制则是递归地进行复制,每一层都是对象复制。因此深复制会复制更多东西,占用更多内存,也比浅复制耗时。以下例子:
[python][进阶之路]理解python中的深复制和浅复制

>>> import copy
>>> b = [1, 2]
>>> a = [b, 3, 4]
>>> d = copy.deepcopy(a)
>>> print(id(d) is id(a))
False
>>> print(id(d[0]) is id(a[0]))
False

d会完全复制a中的所有内容,因此当d和a是两个完全不同的对象,由于递归复制了所有对象,那么显然d[0]和a[0]也是两个不同的对象,两者不相互影响。

引发的问题

python二维数组初始化

如果使用list来初始化一个全0的数组,便很大可能出现问题,对于一维数组,可以简单地使用a = [0] * 3这里就是使用的浅复制。当二维数组时候就会:

>>> a = [[0] * 3] *3
>>> a[0][0] = 1
>>> print(a)
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]

出现与感觉上相违背的事情,原因是这里的[]*n使用的时浅拷贝,当使用嵌套时,则出现这种错误,上述代码相当于:

>>> b = [0] * 3
>>> a = [b, b, b]

正确的初始化方式为生成式[[0 for j in range(3)] for i in range(3)]或者使用nunpy。

元组修改问题

元组是不允许修改的,但是如果有以下写法,则是很容易出现问题。例子:

a = ([1, 2], 3, 4)
a[0] += [5, 6]
print(a)

当运行以上程序时,首先会报错:
TypeError: 'tuple' object does not support item assignment,因为元组不允许被修改,但是当打印时会发现:a = ([1, 2, 5, 6], 3, 4),让人觉得很诡异,明明已经报错了,但是元组内的值还是被修改了。这其实因为是+=并不是一个原子操作,先执行的添加,然后再执行的赋值,当赋值时候添加完成了,但是赋值却不被允许。

引用

[1]. https://docs.python.org/3.6/library/copy.html
[2]. http://wsfdl.com/python/2013/08/16/%E7%90%86%E8%A7%A3Python%E7%9A%84%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B5%85%E6%8B%B7%E8%B4%9D.html
[3]. https://www.cnblogs.com/wildkid1024/p/11093820.html