拷贝的探究

139 阅读11分钟

最近在项目中,因为要对原始变量进行内部子值的删除便于对两个原始变量进行其他操作,使用到拷贝技术,所以对拷贝技术进行一些温习。

拷贝其实在实际项目中会经常使用到,在进行技术面试时也经常会被问到。拷贝技术也会在各种语言中使用到,这里主要是介绍 在python中的应用。

1、预备知识

1.1、变量

在python语言中,讲究万物皆对象,如1,'hello',[1,2,3],{'a':1}等,即使是type本身也是一个对象,构造的任何数据类型都是对象。Python中变量与C/C++/Java中不同,它是指对象的引用,Python是动态类型,程序运行的时候,会根据对象的类型来确认变量到底是什么类型。python中对象都拥有三个属性:身份、类型、值。

>>>a = "hello word"
>>>id(a) # a的身份的标识
1886763127984
>>>type(a) # a的类型str
<class 'str'>
>>>a # a的值
'hello word'

1.2、引用

在高级语言中,变量是对内存及其地址的抽象。python中,一切变量即指对象,对象存储都采用引用方式,存储只是一个对象值的内存地址。

引用实际就是内存中的一个数字地址编号,在使用对象时,只要知道这个对象地址,就可以操作这个对象,但是因为这个数字地址不方便在开发时使用和记忆,所以使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不开辟存储空间。

网上的另一种类似的解释正如访问网站的IP地址,主机实际是通过IP地址来确认的,IP地址不便于记忆,所以以域名来代表IP地址,在使用域名访问网站时,域名被解析成IP地址来使用。域名就相当于变量名,IP地址相当于对象的数字地址。下面例子说明变量和对象指向同一地址:

>>>b = 1
>>>id(b)
1878414112
>>>id(1)
1878414112

在高级语言中,变量是对内存及其地址的抽象。python中,一切变量即指对象,对象存储都采用引用方式,存储只是一个对象值的内存地址。

引用实际就是内存中的一个数字地址编号,在使用对象时,只要知道这个对象地址,就可以操作这个对象,但是因为这个数字地址不方便在开发时使用和记忆,所以使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不开辟存储空间。

网上的另一种类似的解释正如访问网站的IP地址,主机实际是通过IP地址来确认的,IP地址不便于记忆,所以以域名来代表IP地址,在使用域名访问网站时,域名被解析成IP地址来使用。域名就相当于变量名,IP地址相当于对象的数字地址。下面例子说明变量和对象指向同一地址:

>>>b = 1
>>>id(b)
1878414112
>>>id(1)
1878414112

image.png

下图是参考一张地址引用与值引用的对比图

image.png

python中,变量保存的是对象(值)的引用,我们称为引用语义。采用这种方式,变量所需的存储空间大小一致,因为变量只是保存了一个引用。也被称为对象语义和指针语义。值语义:有些语言采用的不是这种方式,它们把变量的值直接保存在变量的存储区里,这种方式被我们称为值语义,例如C语言,采用这种存储方式,每一个变量在内存中所占的空间就要根据变量实际的大小而定,无法固定下来。

专业表述如下:

  • 变量是一个系统表的元素,拥有指向对象的连接的空间
  • 对象是被分配的一块内存,存储其所代表的值
  • 引用是自动形成的从变量到对象的指针 特别注意: 类型属于对象,不是变量

1.3、赋值

赋值语句就是用来赋给某变量一个具体值的语句。Python中,对象的赋值都是进行对象引用(内存地址)传递。在进行了上面变量、引用的介绍,这里就比较清晰了。

>>>a = 'hello word'
>>>print(id(a))
3026940251952
>>>b = a
>>>print(id(b))
3026940251952
>>>a = 'new hello word'
>>>print(a)
new hello word
>>>print(b)
hello word
>>>print(id(a))
3026940248432
>>>print(id(b))
3026940251952

image.png

a再次赋值使得内存地址发生改变,上图看到虽然修改了a,但被赋值的b从内存地址到值没有改变,a,b起初都指向‘hello word’的地址,重新对a赋值使得a存储地址发生变化,指向新建的值,b并未改变。

>>>list1 = [1, 2, 3]
>>>list2 = list1
>>>print(id(list1))
>>>1578432413896
>>>print(id(list2))
>>>1578432413896
>>>list2.append('new value')
>>>print(list1)
>>>[1, 2, 3, 'new value']
>>>print(list2)
>>>[1, 2, 3, 'new value']
>>>print(id(list1))
>>>1578432413896
>>>print(id(list2))
>>>1578432413896

image.png

对列表的增加修改操作,没有改变列表的内存地址,lst1和lst2都发生了变化。对照内存图,列表添加新值,列表又多存储一个新元素的地址,而列表本身地址没有变化,所以lst1和lst2的id均没有改变并且都被添加了一个新的元素。

1.4、数据类型

  数据类型:number(数字)、string(字符串)、list(列表)、tuple(元组)、set(集合)、dictionary(字典)

对象不可变:数字( int、float、bool、complex(复数))、string、元组

可变对象:列表、字典、集合

可变是指可变对象的值可变,身份是不变的。

不可变对象就是对象的身份和值都不可变。新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。

2、主体

变量赋值等于完全共享了一个值改变等于另一个值也得到了共享,遇到实际情况需要保留原始数据一份,再去处理数据,这时就不适合。python为这种情况提供了copy模块。提供两种主要的copy方法,一种是普通的copy,另一种是deepcopy,称前者是浅拷贝,后者为深拷贝。在python直接使用copy.copy与copy.deepcopy函数。

2.1、浅拷贝

这里直接代码来查看:

import copy

A  = ['hello', 1, ['word''python''abc']]
B = copy.copy(A)
print(id(A))
print(A)
print([id(i) for i in A])
print(id(B))
print(B)
print([id(i) for i in B])

A[0] = "lol"
A[2].append("c++")
print(id(A))
print(A)
print([id(i) for i in A])
print(id(B))
print(B)
print([id(i) for i in B])

输出:

140083681098696
['hello', 1, ['word''python''abc']]
[140083685070640, 94547927186240, 140083681094600]
140083681097736
['hello', 1, ['word''python''abc']]
[140083685070640, 94547927186240, 140083681094600]

140083681098696
['lol', 1, ['word''python''abc''c++']]
[140083680693752, 94547927186240, 140083681094600]
140083681097736
['hello', 1, ['word''python''abc''c++']]
[140083685070640, 94547927186240, 140083681094600]

image.png

上面结果分析:A建立list对象,然后使用copy模块浅拷贝函数,,对A指向对象进行浅拷贝赋值给对象B,可以发现B和A内存地址并不相同,但B中对象元素内存地址和A保持一致,也就是浅拷贝会创建一个新的对象,但对于对象中元素,浅拷贝就只会使用原始元素的引用(内存地址)。在进行了A修改,因为list第一个元素为字符串,不可变类型,所以产生新的对象元素地址发生了变化,A的第三个元素为可变类型,修改添加不会产生新的对象,所以A的修改结果会响应到B上。

2.2、深拷贝

再来看看深拷贝:

import copy

A  = ['hello', 1, ['word''python''abc']]
B = copy.deepcopy(A)
print(id(A))
print(A)
print([id(i) for i in A])
print(id(B))
print(B)
print([id(i) for i in B])

A[0] = "lol"
A[2].append("c++")
print(id(A))
print(A)
print([id(i) for i in A])
print(id(B))
print(B)
print([id(i) for i in B])

输出:

140083681091272
['hello', 1, ['word''python''abc']]
[140083685070640, 94547927186240, 140083681093192]
140083681098696
['hello', 1, ['word''python''abc']]
[140083685070640, 94547927186240, 140083681097480]

140083681091272
['lol', 1, ['word''python''abc''c++']]
[140083680693752, 94547927186240, 140083681093192]
140083681098696
['hello', 1, ['word''python''abc']]
[140083685070640, 94547927186240, 140083681097480]

image.png

分析上述代码结果:同样创建A对象list,使用copy中深拷贝函数deepcopy对A进行拷贝生成新对象赋值给B。和浅拷贝类似,深拷贝创建新对象,深拷贝会重新生成一份原始数据并不是简单原始数据(当然有特殊情况)。A的第三个对象指向140083681093192,而B的第三个元素是全新的对象指向140083681097480。当对A进行修改时,以为A的第一个元素是不可变对象,所以会使用一个新的指向地址140083680693752,第三个元素可变类型,修改操作不会产生新的对象,所以A的修改不会影响到B。

一些特殊情况:

  • 对于一些分容器类型(数字,字符串与‘原子’类型的对象)没有拷贝一说

  • 元祖变量只包含原子类型对象,则不能深拷贝,如下所示:

图片

总结

通俗的讲深拷贝就是完全跟以前没有任何关系了,原来的对象怎么改都不会影响当前对象;浅拷贝就是藕断丝连,会对当前产生影响;深拷贝就是离婚了互不干扰。

  • Python中对象赋值都是进行对象引用(内存地址)传递
  • 使用copy.copy(),可进行对象浅拷贝,复制对象,对对象中元素,依然是原始引用
  • 当需要复制一个容器对象,及里面所有元素(包含元素的子元素),可使用copy.deepcopy()进行深拷贝
  • 对于非容器类型(如数字、字符串、和其他’原子’类型的对象)没有被拷贝一说
  • 如果元祖变量只包含原子类型对象,则不能深拷贝

此处分享一个好的网址,可可视化对象赋值拷贝过程变化:

pythontutor.com/

参考链接:

www.cnblogs.com/Eva-J/p/553…

转载自【程序媛的被窝】 mp.weixin.qq.com/s?__biz=Mzk…