浅谈python深拷贝和浅拷贝

最近在面试过程中经常被问到python的浅拷贝和深拷贝,在笔试过程中也因为深拷贝和浅拷贝出现了很多问题,所以这里整理一下有关python深拷贝和浅拷贝的相关知识
主要参考以下几篇博客:

  1. 夏晓旭
  2. 菜鸟教程
  3. python深拷贝和浅拷贝的区别
  4. python不可变对象

在介绍深拷贝和浅拷贝之前需要先理解python的可变和不可变对象

python可变和不可变对象

python中有可变对象和不可变对象。可变对象:list,dict. 不可变对象有:int,string,float,tuple

不可变对象

示例如下:

def viable_test():
	i = 66
	j = 66
	print('66 id :', id(66))
	print('id of i :', id(i))
	print('id of j: ', id(j))
	j = j + 1 ##change the value of j, see whether the id change
	print('changed value id of j:', id(j))	
	j = j -1
	print('j = 66 id:', id(j))
	i = i+1
	print('i = 67 id:', id(i))
	print('66 id:', id(66))		
if __name__ == '__main__':
	viable_test()

输出:

66 id : 1993828544
id of i : 1993828544
id of j:  1993828544
changed value id of j: 1993828576
j = 66 id: 1993828544
i = 67 id: 1993828576
66 id: 1993828544

一开始i,j 和66的地址一样,但是当改变j的值后,j指向的地址也发生了改变,再将j的值变回66,发现j的地址又指向了66在内存中的地址,且将i加一,发现i指向了67的地址,可以发现,对于不可变对象,不支持原处修改(此时我们会发现数值66有一个固定地址,所有值为66的整数均指向这个地址,那么就有一个疑问,python是否将所有的整数都存在内存中?如果是这样,那内存占用不就过大了?)
再看下面一个例子:

def viable_test():
	i = 1000
	j = 1000
	print('1000 id :', id(1000))
	print('id of i :', id(i))
	print('id of j: ', id(j))
	j = j + 1 ##change the value of j, see whether the id change
	print('changed value id of j=1001:', id(j))
	
	j = j -1
	print('changed j = 1000 id:', id(j))
	i = i+1
	print('changed i = 1001 id:', id(i))
	print('1000 id:', id(1000))
		
if __name__ == '__main__':
	viable_test()

输出:

1000 id : 2340535978736
id of i : 2340535978736
id of j:  2340535978736
changed value id of j=1001: 2340504121072
changed j = 1000 id: 2340535977552
changed i = 1001 id: 2340504121072
1000 id: 2340535978736

由此可见:当j加1再减一后,值仍为1000,但是地址却发生了改变
这是因为:

  1. 对于小整数[-5, 256],考虑到小整数可能频繁使用,出于性能考虑,python使用小整数对象缓冲池small_ints缓存了[-5, 257)之间的整数,该范围内的整数在Python系统中是共享的。小整数对象在py启动过程中初始化,这些个小整数对象的ob_refcnt(引用计数记录指向对象引用的个数,当变为0,则被释放,引用计数变量 ob_refcnt)不会改变且永远>0,所以在vm运行过程中不会被销毁,所以起到了缓冲的作用。
  2. 对于超出了[-5, 257)之间的其他整数,Python同样提供了专门的缓冲池(通用整数对象的缓冲池),供这些所谓的大整数使用,避免每次使用的时候都要不断的malloc分配内存带来的效率损耗。通过free-list,管理空闲空间。
  3. 整数对象回收时,内存并不会归还给系统,而是将其对象的ob_type指向free_list,供新创建的对象使用

可变对象

def unviable_test():
	a = {}
	b = a
	print('id of a :', id(a))
	print('id of b:', id(b))
	a['a'] = 'hhh'
	print('id of changed a:', id(a))
	print('id of b:', id(b))
	print('a:', a)
	print('b:', b)		
if __name__ == '__main__':
	unviable_test()

输出:

id of a : 2486235319176
id of b: 2486235319176
id of changed a: 2486235319176
id of b: 2486235319176
a: {'a': 'hhh'}
b: {'a': 'hhh'}

a创建时的内存地址id是2486235319176, 然后把a赋值给b其实就是让变量b的也指向a所指向的内存空间。然后我们发现当a发生变化后,b也跟着发生变化了,因为字典是可变类型,所以并不会复制一份再改变,而是直接在a所指向的内存空间修改数据,而b也是指向该内存空间的,自然b也就跟着改变了。

  • 由于python规定参数传递都是传递引用(C等语言传递参数的时候允许程序员选择值传递还是引用传递,如C语言加上*号传递指针就是引用传递,而直接传递变量名就是值传递,而python只允许使用引用传递),也就是传递给函数的是原变量实际所指向的内存空间,修改的时候就会根据该引用的指向去修改该内存中的内容,所以按道理说我们在函数内改变了传递过来的参数的值的话,原来外部的变量也应该受到影响。但是上面我们说到了python中有可变类型和不可变类型,这样的话,当传过来的是可变类型(list,dict)时,我们在函数内部修改就会影响函数外部的变量。而传入的是不可变类型时在函数内部修改改变量并不会影响函数外部的变量,因为修改的时候会先复制一份再修改。

python深拷贝和浅拷贝

在了解了python的可变对象和不可变对象后,我们再来看看python的深拷贝和浅拷贝,主要区分直接赋值,浅拷贝,深拷贝

  • 直接赋值:对象的引用
  • 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象,所以原始数据改变,子对象也会改变
  • 深拷贝(deepcopy):完全拷贝父对象及其子对象,所以原始对象的改变不会造成深拷贝里任何子元素的改变。相当于在内存中又开辟了一个空间,把原数据拷贝进去

看下面几个例子:

import copy
a = 6
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print('value of a,b,c,d:', a,b,c,d)
print('id of a:', id(a))
print('id of b:', id(b))
print('id of c:', id(c))
print('id of d:', id(d))
a = 7
print('value of a,b,c,d:', a,b,c,d)
print('id of a:', id(a))
print('id of b:', id(b))
print('id of c:', id(c))
print('id of d:', id(d))

输出:

value of a,b,c,d: 6 6 6 6
id of a: 1993826624
id of b: 1993826624
id of c: 1993826624
id of d: 1993826624
value of a,b,c,d: 7 6 6 6
id of a: 1993826656
id of b: 1993826624
id of c: 1993826624
id of d: 1993826624

对于不可变对象a,对其赋值,浅拷贝,深拷贝后均指向同一个地址,当改变a的值后,实际是将a复制一份变为7存到另一个地方,所以b,c,d的值都不会改变,此时,赋值和浅拷贝,深拷贝无区别,那么对于不可变对象呢?话不多说,先看一个例子

import copy
a = [1,2,3,['a','b','c']]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print('value of a:', a)
print('value of b:', b)
print('value of c:', c)
print('value of d:', d)
print('id of a:', id(a))
print('id of b:', id(b))
print('id of c:', id(c))
print('id of d:', id(d))
a.append(4)
print('value of a:', a)
print('value of b:', b)
print('value of c:', c)
print('value of d:', d)
print('id of a:', id(a))
print('id of b:', id(b))
print('id of c:', id(c))
print('id of d:', id(d))

输出:

value of a: [1, 2, 3, ['a', 'b', 'c']]
value of b: [1, 2, 3, ['a', 'b', 'c']]
value of c: [1, 2, 3, ['a', 'b', 'c']]
value of d: [1, 2, 3, ['a', 'b', 'c']]
id of a: 1639475625480
id of b: 1639475625480
id of c: 1639475717768
id of d: 1639475716744
value of a: [1, 2, 3, ['a', 'b', 'c'], 4]
value of b: [1, 2, 3, ['a', 'b', 'c'], 4]
value of c: [1, 2, 3, ['a', 'b', 'c']]
value of d: [1, 2, 3, ['a', 'b', 'c']]
id of a: 1639475625480
id of b: 1639475625480
id of c: 1639475717768
id of d: 1639475716744

由此可见,对可变对象直接赋值,就是引用,b的id和a的id相同,当a改变后,b的值也会发生改变
浅拷贝:

import copy
a = [1,2,3,['a','b','c']]
c = copy.copy(a)
print('value of a:', a)
print('value of c:', c)
print('id of a:', id(a))
print('id of c:', id(c))
print('id of a[0]:', id(a[0]))
print('id of c[0]:', id(c[0]))
a.append(4)
print('value of a:', a)
print('value of c:', c)
print('id of a[0]:', id(a[0]))
print('id of c[0]:', id(c[0]))
a[3].append('ddd')
print('value of a:', a)
print('value of c:', c)

输出:

value of a: [1, 2, 3, ['a', 'b', 'c']]
value of c: [1, 2, 3, ['a', 'b', 'c']]
id of a: 2059054398728
id of c: 2059054490760
id of a[0]: 1993826464
id of c[0]: 1993826464
value of a: [1, 2, 3, ['a', 'b', 'c'], 4]
value of c: [1, 2, 3, ['a', 'b', 'c']]
id of a[0]: 1993826464
id of c[0]: 1993826464
value of a: [1, 2, 3, ['a', 'b', 'c', 'ddd'], 4]
value of c: [1, 2, 3, ['a', 'b', 'c', 'ddd']]

浅拷贝:拷贝出来的新对象的地址和原对象是不一样的,但是新对象里面的可变元素(如列表)的地址和原对象里的可变元素的地址是相同的,也就是说浅拷贝它拷贝的是浅层次的数据结构(不可变元素),对象里的可变元素作为深层次的数据结构并没有被拷贝到新地址里面去,而是和原对象里的可变元素指向同一个地址,所以在新对象或原对象里对这个可变元素做修改时,两个对象是同时改变的

import copy
a = [1,2,3,['a','b','c']]
c = copy.deepcopy(a)
print('value of a:', a)
print('value of c:', c)
print('id of a:', id(a))
print('id of c:', id(c))
print('id of a[0]:', id(a[0]))
print('id of c[0]:', id(c[0]))
a.append(4)
print('value of a:', a)
print('value of c:', c)
print('id of a[0]:', id(a[0]))
print('id of c[0]:', id(c[0]))
a[3].append('ddd')
print('value of a:', a)
print('value of c:', c)

输出:

value of a: [1, 2, 3, ['a', 'b', 'c']]
value of c: [1, 2, 3, ['a', 'b', 'c']]
id of a: 1831628029192
id of c: 1831628121288
id of a[0]: 1993826464
id of c[0]: 1993826464
value of a: [1, 2, 3, ['a', 'b', 'c'], 4]
value of c: [1, 2, 3, ['a', 'b', 'c']]
id of a[0]: 1993826464
id of c[0]: 1993826464
value of a: [1, 2, 3, ['a', 'b', 'c', 'ddd'], 4]
value of c: [1, 2, 3, ['a', 'b', 'c']]

深拷贝:另外开辟一个内存空间,拷贝原对象,所以原对象做任何的改变都不会影响深拷贝后的对象

  • Python中有多种方式实现浅拷贝,copy模块的copy函数、对象的copy函数、工厂方法、切片等;大多数情况下,编写程序时都是使用浅拷贝,除非有特定的需求;浅拷贝的优点:拷贝速度快,占用空间少,拷贝效率高

版权声明:本文为qq_36402402原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。