【Python基础】Python的深浅拷贝讲解
共 3567字,需浏览 8分钟
· 2020-12-11
前言
在很多语言中都存在深浅拷贝两种拷贝数据的方式,Python中也不例外。本文中详细介绍了Python中的深浅拷贝的相关知识,文章的内容包含:
对象、数据类型、引用 赋值 浅拷贝 深拷贝
![](https://filescdn.proginn.com/cc6d99ab114acacd046d86d89b0163e1/b6aa3b11635ccfec9b6575602dcb4d05.webp)
一、Python对象
我们经常听到:在Python中一切皆对象。其实,说的就是我们在Python中构造的任何数据类型都是一个对象,不管是数字、字符串、字典等常见的数据结构,还是函数,甚至是我们导入的模块等,Python都会把它当做是一个对象来处理。
所有的Python对象都拥有3个属性:
身份 类型 值
我们看一个简单的例子来理解上面的3个属性:
假设我们声明了一个name变量,通过id、type方法能够查看对象的身份和类型:
![](https://filescdn.proginn.com/8ef69ea8caa3d976647ba8dc9134e4f4/37d47c1b9b690f27033b959c29f21c4a.webp)
甚至是type本身也是一个对象,它也拥有自己的身份、类型:
![](https://filescdn.proginn.com/0260cc978df63bfebba57660ea962632/eaf282e96cb65f483979849643a7aadd.webp)
Python中,万物皆对象
二、数据类型
2.1 可变和不可变类型
在Python中,按照更新对象的方式,我们可以将对象分为2大类:可变数据类型和不可变数据类型。
不可变数据类型:数值、字符串、布尔值。不可变对象就是对象的身份和值都不可变。新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。
可变数据类型:列表、字典、集合。所谓的可变指的是可变对象的值可变,但是身份是不可变的。
首先我们看看不可变对象:
![](https://filescdn.proginn.com/089b711b7f116dc0071f55b3e600db4e/15d7b7927a5dc50f34563fd9582bdce7.webp)
当我们定义了一个对象str1,给其赋值了“python”,便会在内存中找到一个固定的内存地址来存放;但是,当我们将“python”定义成另一个变量名的时候,我们发现:它在内存中的位置是不变的。
![](https://filescdn.proginn.com/dea4cfc86103d1ec1f8d6ba2944cdbcd/7ac9be4cc10ffd559db306d08dc44333.webp)
也就是说,这个变量在计算机内存中的位置是不变的,只是换了一个名字来存放,来看3个实际的例子:
![](https://filescdn.proginn.com/7b8d8388e7eef1009df1b401c1286174/71cd60c3ec82fa8d637caea9eb043ea7.webp)
![](https://filescdn.proginn.com/69dee7c14688b0cd82836dec8818d2ba/b9435e6614e779693c0fa2d432970371.webp)
![](https://filescdn.proginn.com/3dcfbb6f8aeb927479e234ccfdb70b30/b7dbabe17724e6c9d7d99e15174817b0.webp)
以上的例子说明:当我们对字符串、数值型、布尔值的数据改变变量名,并不会影响到数据在内存中的位置。
我们看看可变类型的例子,列表、字典、集合都是一样的效果:
![](https://filescdn.proginn.com/350d089282dbc0541ce05b51fb3db097/4ef2e404b4a818392ff8ec40c565c08f.webp)
![](https://filescdn.proginn.com/44b31eb784eba43116285751d9469760/b4a7ccf818763b85180cd829c20d386b.webp)
![](https://filescdn.proginn.com/b0e8c23d75223dce772c53588421d86a/fa0e439841fc691ba104f00dc27a3302.webp)
虽然是相同的数据,但是变量名字不同,内存中仍然会开辟新的内存地址来进行存放相同的数据,我们以字典为例:
![](https://filescdn.proginn.com/9e33b83f8f4732a5c895406a0f5e1d68/4f84ec8489a5435e5c1f45394a9f293b.webp)
2.2 引用
在Python语言中,每个对象都会在内存中申请开辟一块新的空间来保存对象;对象在内存中所在位置的地址称之为引用。
可以说,我们定义的变量名实际上就是对象的地址引用。引用实际上就是内存中的一个数字地址编号。在使用对象的时候,只要知道这个对象的地址,我们就可以操作这个对象。
因为这个数字地址不太容易记忆,所以我们使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不会开辟新的存储空间。
我们通过一个例子来说明变量和变量指向的引用(内存地址)实际上就是一个东西:
![](https://filescdn.proginn.com/0403174f558c288a6d5f389f6c6d5b09/93794a257569a46462d76a1431f7866c.webp)
![](https://filescdn.proginn.com/0a26fca6bbd79bb2a700c69de47421f7/207f53429f429934034810a16f879ed8.webp)
三、赋值
3.1 相同数据,不同变量名
讨论完Python的对象、属性和引用3个重要的概念之后,在正式介绍深浅拷贝之前,我们先讨论Python中的赋值。
在Python中,每次赋值都会开辟新的内存地址来存放数据,比如我们同时存放一个列表[1,2,3],即使数据是相同的,但是内存地址却不同:
![](https://filescdn.proginn.com/74175d8812adbfcd201285f9fabadcfe/1080bb79a22693957f3a5136f9c8f56b.webp)
其实就是两个不同的变量,只是恰好它们存放了相同的数据而已,但是存放的地址是不同的。
![](https://filescdn.proginn.com/8bc94f29b206a68f9df2ee758c2d130f/0925beace680e8d85d3c5645b3ae3037.webp)
我们给v1列表追加了一个元素,发现它的内存地址是不变的,当然v2肯定是不变的:
![](https://filescdn.proginn.com/96ec9453b1d5880192b0c10f052631ec/e20b821c71cdbd562e9d540454d52d3d.webp)
![](https://filescdn.proginn.com/31d4b946891ae53a2e96083ad9c7564c/bdc9d4bdd5596d14bcab86eeefdd7d6a.webp)
3.2 一个变量多次赋值
如果我们对一个变量多次赋值,其内存是会变化的:
![](https://filescdn.proginn.com/184c7e78e1715ce5ccaf863c9332143c/991338b2f30418f60fdafc340ea18390.webp)
![](https://filescdn.proginn.com/06670ec1dfd5cb6071f2ae9d898268d9/8e2dbd2dd8e571b82a8bf4180602b086.webp)
3.3 变量赋值
将一个变量赋值给另一个变量,其实它们就是同一个对象:数据相同,在内存中的地址也相同:
![](https://filescdn.proginn.com/4b6bf02ac3bbe2c9f3a7632d0d24f042/f18aed928c09d14c9c074e564df0fc59.webp)
![](https://filescdn.proginn.com/0dbb46287537577c612e1d97f4ab651c/3ef0c7af2171a449d3a642960b9ed33e.webp)
当我们给V1追加一个元素,V2也会同时变化:
![](https://filescdn.proginn.com/8bfb09a35778c5a1dcb6b4d5690f4b1a/2b7277b4ee539ddc3b9b0caf2674f603.webp)
实际上它们就是同一个对象!!!!
3.4 嵌套赋值
如果是列表中嵌套着另外的列表,那么当改变其中一个列表的时候,另一个列表中的也会随着改变:
![](https://filescdn.proginn.com/2759693db92b7eb2f70630789741f339/189c171d29031376ff4282b23acde9fa.webp)
原始数据信息:
![](https://filescdn.proginn.com/a0becf13f668bde61f0d0c62337338c4/e882b81cc61dd119815b14672e4ab0e9.webp)
当我们给v1追加了新元素之后:
![](https://filescdn.proginn.com/3152b00bb1d85b8aea2d359c19ed2f33/88d410ec92da6eefc891c535fd5a9b4a.webp)
总结:赋值其实就是将一个对象的地址赋值给一个变量,使得变量指向该内存地址。
四、浅拷贝
在Python中进行拷贝之前,我们需要导入模块:
import copy
⚠️浅拷贝只是拷贝数据的第一层,不会拷贝子对象。
4.1 不可变类型的浅拷贝
如果只是针对不可变的数据类型(字符串、数值型、布尔值),浅拷贝的对象和原数据对象是相同的内存地址:
![](https://filescdn.proginn.com/9c2733565c420f7b256f1095b8f2bbb6/c2e6c09edd8d15d2625be83695c6bd4e.webp)
![](https://filescdn.proginn.com/842c1b68bd22806ffeea0b5d136b6e0b/39e99f89a988a8994430e60a44437de2.webp)
从上面的结果中我们可以看出来:针对不可变类型的浅拷贝,只是换了一个名字,对象在内存中的地址其实是不变的。
![](https://filescdn.proginn.com/29f9ef76b52167435612caf77d0e40e4/05cc3c1479fdde824bb90b5dd97fd61e.webp)
4.2 可变类型的浅拷贝
首先我们讨论的是不存在嵌套类型的可变类型数据(列表、字典、集合):
![](https://filescdn.proginn.com/bf4246e20d52a1c9175f441d66a64542/9e6d01c487e444c5651c008f295663aa.webp)
从上面的例子看出来:
列表本身的浅拷贝对象的地址和原对象的地址是不同的,因为列表是可变数据类型。 列表中的元素(第1个元素为例)和浅拷贝对象中的第一个元素的地址是相同的,因为元素本身是数值型,是不可变的。
通过一个图形来说明这个关系:
![](https://filescdn.proginn.com/71f06831a762c1b2be9e4d548e4ef759/7e901429e243ce8aaeba62c62124fb2d.webp)
字典中也存在相同的情况:字典本身的内存地址不同,但是里面的键、值的内存地址是相同的,因为键值都是不可变类型的数据。
![](https://filescdn.proginn.com/bc81047e5b132f61a7315382a59fde66/982cc3d422bab7ebbeb28f4ec253ab3a.webp)
如果可变类型的数据中存在嵌套的结构:
![](https://filescdn.proginn.com/85c1d558a22283249eaa1f79f3257749/eb27d011c66d53d4331ebad18f0400a2.webp)
从上面的两个例子中我们可以看出来:
在可变类型的数据中,如果存在嵌套的结构类型,浅拷贝只复制最外层的数据,导致内存地址发生变化,里面数据的内存地址不会变。
五、深拷贝
深拷贝不同于浅拷贝的是:深拷贝会拷贝所有的可变数据类型,包含嵌套的数据中的可变数据。深拷贝是变量对应的值复制到新的内存地址中,而不是复制数据对应的内存地址。
5.1 不可变类型的深拷贝
关于不可变类型的深浅拷贝,其效果是相同的,具体看下面的例子:
![](https://filescdn.proginn.com/29caded4fe96779080cddda9d5294ae7/e66f85bf3e27320ba30776270e84ca75.webp)
![](https://filescdn.proginn.com/91ef8dccf979f8b64b7a173d5f1da66b/2519d9614b8937ce90ce475166e9b9b4.webp)
![](https://filescdn.proginn.com/43c25da0606f0281a2097824dfa94985/e3fa1f7b8d5d49e4beb07fccb35db57e.webp)
我们得出一个结论:针对不可变数据类型的深浅拷贝,其结果是相同的。
5.2 可变类型的深拷贝
首先我们讨论的是不存在嵌套的情况:
针对列表数据:
![](https://filescdn.proginn.com/b2b9223d9c3c527e6ae53e226bad9cbb/3fc23367d76ea990ddcef1b457ffed9e.webp)
![](https://filescdn.proginn.com/6cf01551ecf5e242d7b6def539200b76/f065a0c20e50dbcae1b8d836cb045082.webp)
针对字典数据:
![](https://filescdn.proginn.com/e2534ad726b627cf080c3f5a6beef0cf/3f4f03d3e2db3ecfb1b30c37bb94b090.webp)
![](https://filescdn.proginn.com/8d55dbc1af6d3f0f854b8b93c4bccda3/4d6a81023b0dbbe4cd24aed2cdac64c6.webp)
我们可以得出结论:
深拷贝对最外层数据是只拷贝数据,会开辟新的内存地址来存放数据。 深拷贝对里面的不可变数据类型直接复制数据和地址,和可变类型的浅拷贝是相同的效果。
![](https://filescdn.proginn.com/7ed90f53f2d95225301b958113ba93cc/99a9154ab3312490e972ec213dc77273.webp)
我们讨论存在嵌套类型的深拷贝(以列表为例)。
![](https://filescdn.proginn.com/42d6e105f6f034269d0b80bef1471717/9b270502b6c5cc654818a5c25d93cb30.webp)
结论1:对整个存在嵌套类型的数据进行深浅拷贝都会发生内存的变化,因为数据本身是可变的。
![](https://filescdn.proginn.com/ac33d359731b6a93467a1bc840d8368b/f23630d1f17bf1ab8d6e9914751ba36e.webp)
结论2:我们查看第一个元素1的内存地址,发生三者是相同的,因为1是属于数值型,是不可变类型。
![](https://filescdn.proginn.com/ab296986dc8a544b6050271c5d9e6863/a637f6c516e0bc312d1cd0b2e3914ceb.webp)
结论3:我们查看第三个元素即里面嵌套列表的内存,发现只有深拷贝是不同的,因为这个嵌套的列表是可变数据类型,深拷贝在拷贝了最外层之后还会继续拷贝子层级的可变类型。
![](https://filescdn.proginn.com/75cc50e9804de8d9049aa5e83c924133/92687617269950f20bd809b2cf0338c8.webp)
结论4:我们查看嵌套列表中的元素的内存地址,发现它们是相同的,因为元素是数值型,是不可变的,不受拷贝的影响。
六、元组的深浅拷贝
元组本身是不可变数据类型,但是其中的值是可以改变的,内部可以有嵌套可变数据类型,比如列表等,会对它的拷贝结果造成影响。
6.1 不存在嵌套结构
当元组中不存在嵌套结构的时候,元组的深浅拷贝是相同的效果:
![](https://filescdn.proginn.com/187a81f988bfb188b287de4e3aa71a38/eac9af2c771d5dfd0114256ff24c71c2.webp)
6.2 存在嵌套结构
当元组的数据中存在嵌套的可变类型,比如列表等,深拷贝会重新开辟地址,将元组重新成成一份。
![](https://filescdn.proginn.com/9ea7f1cfaf3d5666e13f336cd4069e9c/ef8c4afb6523b27cfbb07d5caf74b894.webp)
七、is和==
在文章的开始就已经谈过:在Python中每个变量都有自己的标识、类型和值。每个对象一旦创建,它的标识就绝对不会变。一个对象的标识,我们可以理解成其在内存中的地址。is()
运算符比较的是两个对象的标识;id()
方法返回的就是对象标识的整数表示。
总结:is()
比较对象的标识;==
运算符比较两个对象的值(对象中保存的数据)。在实际的编程中,我们更多关注的是值,而不是标识本身。
第一个例子:我们创建了两个不同的对象,只是它们的值刚好相同而已。
![](https://filescdn.proginn.com/7f347e0cd13bee47547e53b89a3005ea/c3b49b8e7e9e7652c28984a09024e4f3.webp)
![](https://filescdn.proginn.com/067def46dca937e30ae92dff6342f9af/baae08d1d345de3c6dec16e2a710fc26.webp)
第二个例子:我们先创建了一个对象v3,然后将他赋值给另一个对象v4,其实它们就是相同的对象,所以标识(内存地址)是相同的,只是它们的名字不同而已。
![](https://filescdn.proginn.com/374e2b9b8429ea98a2ad63b65303cb63/98ca886f8f4cab26858217a620d06382.webp)
![](https://filescdn.proginn.com/ebb3b53face1a5147bbeb8ade4625629/5bb92c32135bc29c476e1b2a3633c4d8.webp)
总结
通过大量的例子,我们得出结论:
在不可变数据类型中,深浅拷贝都不会开辟新的内存空间,用的都是同一个内存地址。 在存在嵌套可变类型的数据时,深浅拷贝都会开辟新的一块内存空间;同时,不可变类型的值还是指向原来的值的地址。
不同的是:在嵌套可变类型中,浅拷贝只会拷贝最外层的数据,而深拷贝会拷贝所有层级的可变类型数据。
往期精彩回顾
获取本站知识星球优惠券,复制链接直接打开:
https://t.zsxq.com/qFiUFMV
本站qq群704220115。
加入微信群请扫码: