前言
在很多语言中都存在深浅拷贝两种拷贝数据的方式,Python中也不例外。本文中详细介绍了Python中的深浅拷贝的相关知识,文章的内容包含:
- 对象、数据类型、引用
- 赋值
- 浅拷贝
- 深拷贝
![1be42479e895a737adeb277081295475.png](https://i-blog.csdnimg.cn/blog_migrate/53beda3de98220ba6ede4b8bd7724ad5.jpeg)
一、Python对象
我们经常听到:在Python中一切皆对象。其实,说的就是我们在Python中构造的任何数据类型都是一个对象,不管是数字、字符串、字典等常见的数据结构,还是函数,甚至是我们导入的模块等,Python都会把它当做是一个对象来处理。
所有的Python对象都拥有3个属性:
- 身份
- 类型
- 值
我们看一个简单的例子来理解上面的3个属性:
假设我们声明了一个name变量,通过id、type方法能够查看对象的身份和类型:
![4e1a41578eaaa9b709a9b1e74f8c4bae.png](https://i-blog.csdnimg.cn/blog_migrate/39ca341dcaf4107bde5ba25d70f28818.jpeg)
甚至是type本身也是一个对象,它也拥有自己的身份、类型:
![74631e52733a47a73fad5b7c4a3d50eb.png](https://i-blog.csdnimg.cn/blog_migrate/0cea1067fb05f9c1f4fe5a995ba838c8.jpeg)
Python中,万物皆对象
二、数据类型
2.1 可变和不可变类型
在Python中,按照更新对象的方式,我们可以将对象分为2大类:可变数据类型和不可变数据类型。
不可变数据类型:数值、字符串、布尔值。不可变对象就是对象的身份和值都不可变。新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。
可变数据类型:列表、字典、集合。所谓的可变指的是可变对象的值可变,但是身份是不可变的。
首先我们看看不可变对象:
![dff35a19705fb559b1d3489be155d227.png](https://i-blog.csdnimg.cn/blog_migrate/33d10b51ad62470eaa5542055c114a9b.jpeg)
当我们定义了一个对象str1,给其赋值了“python”,便会在内存中找到一个固定的内存地址来存放;但是,当我们将“python”定义成另一个变量名的时候,我们发现:它在内存中的位置是不变的。
![bd022a5a10f8227a3602a26c377f118c.png](https://i-blog.csdnimg.cn/blog_migrate/107b7cb2e299cc91090f51064c05d41d.jpeg)
也就是说,这个变量在计算机内存中的位置是不变的,只是换了一个名字来存放,来看3个实际的例子:
![46670ca2a9e6ab7896e8461e26657578.png](https://i-blog.csdnimg.cn/blog_migrate/1f9c86f3863e22893983290f5170bcc8.jpeg)
![45993200750ea8b31371d771d0524fb4.png](https://i-blog.csdnimg.cn/blog_migrate/801e0f91df7b5537ac29de7448a7cdac.jpeg)
![35594687aeccfcd0d4ecc56ce97b0f36.png](https://i-blog.csdnimg.cn/blog_migrate/3a4ec9f993e45cd32492166e9a9f6bde.jpeg)
以上的例子说明:当我们对字符串、数值型、布尔值的数据改变变量名,并不会影响到数据在内存中的位置。
我们看看可变类型的例子,列表、字典、集合都是一样的效果:
![d25ef92625cf9841ba5cd72da1d978a3.png](https://i-blog.csdnimg.cn/blog_migrate/e9ad5445c83be116cb50e134138b87cd.jpeg)
![e1e01577b424aef69ea6d8df499c70b6.png](https://i-blog.csdnimg.cn/blog_migrate/89d5a2187320e9aa7f9056cedd28f8dc.jpeg)
![9dd9099b69c6aad751b700b8d7678e6f.png](https://i-blog.csdnimg.cn/blog_migrate/208d5ed538bf0cc27ae966572b59ecc6.jpeg)
虽然是相同的数据,但是变量名字不同,内存中仍然会开辟新的内存地址来进行存放相同的数据,我们以字典为例:
![415ff263219dc7ab20754bef6544faad.png](https://i-blog.csdnimg.cn/blog_migrate/2dddfd3c6fa1989aff148c6584ac83fe.jpeg)
2.2 引用
在Python语言中,每个对象都会在内存中申请开辟一块新的空间来保存对象;对象在内存中所在位置的地址称之为引用。
可以说,我们定义的变量名实际上就是对象的地址引用。引用实际上就是内存中的一个数字地址编号。在使用对象的时候,只要知道这个对象的地址,我们就可以操作这个对象。
因为这个数字地址不太容易记忆,所以我们使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不会开辟新的存储空间。
我们通过一个例子来说明变量和变量指向的引用(内存地址)实际上就是一个东西:
![4904296f60fe0460838c4f6e5dd1f5de.png](https://i-blog.csdnimg.cn/blog_migrate/dcef3f4aa49d7330fa252615b4180521.jpeg)
![909391e8a9afad44419f2ff187cf2202.png](https://i-blog.csdnimg.cn/blog_migrate/e12f67751498154b40f6a25dd2a325c5.jpeg)
三、赋值
3.1 相同数据,不同变量名
讨论完Python的对象、属性和引用3个重要的概念之后,在正式介绍深浅拷贝之前,我们先讨论Python中的赋值。
在Python中,每次赋值都会开辟新的内存地址来存放数据,比如我们同时存放一个列表[1,2,3],即使数据是相同的,但是内存地址却不同:
![93a3303bfc7f0db9642f192fed6b1a9f.png](https://i-blog.csdnimg.cn/blog_migrate/a3697c60644f213588162e86d93e24a9.jpeg)
其实就是两个不同的变量,只是恰好它们存放了相同的数据而已,但是存放的地址是不同的。
![977378bfcf6d05df3d1ba968d0df9bb4.png](https://i-blog.csdnimg.cn/blog_migrate/2323666a9a0af7a8599c9417b86feb18.jpeg)
我们给v1列表追加了一个元素,发现它的内存地址是不变的,当然v2肯定是不变的:
![bfe2b1e54c98fbc929a56b90c3f6e223.png](https://i-blog.csdnimg.cn/blog_migrate/58b6b3e2b6d34c4566ff4d18ba42257b.jpeg)
![b4b4651ee04120c492863a1bdf749237.png](https://i-blog.csdnimg.cn/blog_migrate/8d6db94df7436f633d1a295e64ababaa.jpeg)
3.2 一个变量多次赋值
如果我们对一个变量多次赋值,其内存是会变化的:
![c048621633ad82e4a6acc1fcae136532.png](https://i-blog.csdnimg.cn/blog_migrate/4f4e9e6e6523207b861cf04da943d236.jpeg)
![2fccbd1976c10c3f830127641fc7bd59.png](https://i-blog.csdnimg.cn/blog_migrate/cf60ace1d01a2046a1c13bfaed52e7e8.jpeg)
3.3 变量赋值
将一个变量赋值给另一个变量,其实它们就是同一个对象:数据相同,在内存中的地址也相同:
![971ccb8970217b3445f7f085b46c70ae.png](https://i-blog.csdnimg.cn/blog_migrate/945d25e1dffec9beadf9fbd5df3fc5cf.jpeg)
![d3fc6d0244450c285b3eb6b9344b7f1c.png](https://i-blog.csdnimg.cn/blog_migrate/a0c34e4e761355dd2021b90b7bd44b6a.jpeg)
当我们给V1追加一个元素,V2也会同时变化:
![c5fb2ee5d82ea1471f1da6f6de23cb51.png](https://i-blog.csdnimg.cn/blog_migrate/b27fc35977accbdfb1d93e77d5e71295.jpeg)
实际上它们就是同一个对象!!!!
3.4 嵌套赋值
如果是列表中嵌套着另外的列表,那么当改变其中一个列表的时候,另一个列表中的也会随着改变:
![d2691fda80c51d98a16984e8f83935fd.png](https://i-blog.csdnimg.cn/blog_migrate/0de1261f941a4247e34bbd1f2b2a5416.jpeg)
原始数据信息:
![f3ba9e64f377a5a0761d87cc1a22bc7a.png](https://i-blog.csdnimg.cn/blog_migrate/554369a66f725755154019436854ef26.jpeg)
当我们给v1追加了新元素之后:
![65c06801ed281f553877effb5d8d641f.png](https://i-blog.csdnimg.cn/blog_migrate/366415cf443bbb02cd3af822a51985ee.jpeg)
总结:赋值其实就是将一个对象的地址赋值给一个变量,使得变量指向该内存地址。
四、浅拷贝
在Python中进行拷贝之前,我们需要导入模块:
import copy
⚠️浅拷贝只是拷贝数据的第一层,不会拷贝子对象。
4.1 不可变类型的浅拷贝
如果只是针对不可变的数据类型(字符串、数值型、布尔值),浅拷贝的对象和原数据对象是相同的内存地址:
![5f8f71fe7894ff858704d3ab78971d0a.png](https://i-blog.csdnimg.cn/blog_migrate/40fa965fb55723699b0d4de708578d46.jpeg)
![64bfef568f69a10898af227e0698afd5.png](https://i-blog.csdnimg.cn/blog_migrate/9e0ed7bad4f43845f91bade9b6161e00.jpeg)
从上面的结果中我们可以看出来:针对不可变类型的浅拷贝,只是换了一个名字,对象在内存中的地址其实是不变的。
![f964fcb6770229815e8b3444b76e40ff.png](https://i-blog.csdnimg.cn/blog_migrate/6eee12264a58a8fa059a45ed0fbb4924.jpeg)
4.2 可变类型的浅拷贝
首先我们讨论的是不存在嵌套类型的可变类型数据(列表、字典、集合):
![d3eb15d752606499998f94afe98f3894.png](https://i-blog.csdnimg.cn/blog_migrate/ba20c4f5ed450bd858be5d10fd500e70.jpeg)
从上面的例子看出来:
- 列表本身的浅拷贝对象的地址和原对象的地址是不同的,因为列表是可变数据类型。
- 列表中的元素(第1个元素为例)和浅拷贝对象中的第一个元素的地址是相同的,因为元素本身是数值型,是不可变的。
通过一个图形来说明这个关系:
![6dd0f171d8b0bbc80517294df51fa47c.png](https://i-blog.csdnimg.cn/blog_migrate/259ddb7477c8ab5ab9acf34d5037ac95.jpeg)
字典中也存在相同的情况:字典本身的内存地址不同,但是里面的键、值的内存地址是相同的,因为键值都是不可变类型的数据。
![72e85a3c08a2dd904bbeeeac68a7f6b9.png](https://i-blog.csdnimg.cn/blog_migrate/acc070efd893a6938854d8170e396790.jpeg)
如果可变类型的数据中存在嵌套的结构:
![7a88285d04de3a8c1a5e8811686ca873.png](https://i-blog.csdnimg.cn/blog_migrate/d36b68a07ebf5c433f66c3e9abd1c679.jpeg)
从上面的两个例子中我们可以看出来:
在可变类型的数据中,如果存在嵌套的结构类型,浅拷贝只复制最外层的数据,导致内存地址发生变化,里面数据的内存地址不会变。
五、深拷贝
深拷贝不同于浅拷贝的是:深拷贝会拷贝所有的可变数据类型,包含嵌套的数据中的可变数据。深拷贝是变量对应的值复制到新的内存地址中,而不是复制数据对应的内存地址。
5.1 不可变类型的深拷贝
关于不可变类型的深浅拷贝,其效果是相同的,具体看下面的例子:
![d676def0484542d46e25740fe556c815.png](https://i-blog.csdnimg.cn/blog_migrate/2b983b74b8f68b0b21a5b8742d83b27f.jpeg)
![4d48e4a7e0c058ad6514783040765882.png](https://i-blog.csdnimg.cn/blog_migrate/2fcf5b557cb2f3a5cfb39474526f298e.jpeg)
![d670af7df2c38ef2ec2d9d7849424893.png](https://i-blog.csdnimg.cn/blog_migrate/fdb9a856536a4005135f619865c5c5fc.jpeg)
我们得出一个结论:针对不可变数据类型的深浅拷贝,其结果是相同的。
5.2 可变类型的深拷贝
首先我们讨论的是不存在嵌套的情况:
针对列表数据:
![facfd01d025e737bc60386055e3ed001.png](https://i-blog.csdnimg.cn/blog_migrate/e8f9ab37bd1af0b84a342d172689fd11.jpeg)
![dd9191899adf304b20a4ba2688b252ee.png](https://i-blog.csdnimg.cn/blog_migrate/8223f0c3e9696bf2a7fd418100756dcf.jpeg)
针对字典数据:
![db127362cf979c3001e65f65e381106b.png](https://i-blog.csdnimg.cn/blog_migrate/c02b63565ff8567d1a5ec47ea3b8391a.jpeg)
![04ac3e6874d1db7ad9a15c844c6f3529.png](https://i-blog.csdnimg.cn/blog_migrate/c2c9e5c4b5e2970306b3846b54fb499c.jpeg)
我们可以得出结论:
- 深拷贝对最外层数据是只拷贝数据,会开辟新的内存地址来存放数据。
- 深拷贝对里面的不可变数据类型直接复制数据和地址,和可变类型的浅拷贝是相同的效果。
![4daf07e694751b169cb74a213d4c3c53.png](https://i-blog.csdnimg.cn/blog_migrate/7443fbdea80a9c8cfb7c9a4d20502a6b.jpeg)
我们讨论存在嵌套类型的深拷贝(以列表为例)。
![d914e0269c70df7c5d5c106722b2fbc8.png](https://i-blog.csdnimg.cn/blog_migrate/db2eed4086349f68e1af240830d62baa.jpeg)
结论1:对整个存在嵌套类型的数据进行深浅拷贝都会发生内存的变化,因为数据本身是可变的。
![e464f623ad0ec8147c12d6ba530c9d37.png](https://i-blog.csdnimg.cn/blog_migrate/26ec2a326c8b305033f8a3390114d7ca.jpeg)
结论2:我们查看第一个元素1的内存地址,发生三者是相同的,因为1是属于数值型,是不可变类型。
![75965e72d5c9d65d54099696585c0943.png](https://i-blog.csdnimg.cn/blog_migrate/9efaa7053394e9c90ddfed65a5fd83c2.jpeg)
结论3:我们查看第三个元素即里面嵌套列表的内存,发现只有深拷贝是不同的,因为这个嵌套的列表是可变数据类型,深拷贝在拷贝了最外层之后还会继续拷贝子层级的可变类型。
![3214887a55653718351f6b2e04445345.png](https://i-blog.csdnimg.cn/blog_migrate/301b1db6806e1030ba1aeb62f4067c25.jpeg)
结论4:我们查看嵌套列表中的元素的内存地址,发现它们是相同的,因为元素是数值型,是不可变的,不受拷贝的影响。
六、元组的深浅拷贝
元组本身是不可变数据类型,但是其中的值是可以改变的,内部可以有嵌套可变数据类型,比如列表等,会对它的拷贝结果造成影响。
6.1 不存在嵌套结构
当元组中不存在嵌套结构的时候,元组的深浅拷贝是相同的效果:
![e3348574b1510315bd0a184362ee14f4.png](https://i-blog.csdnimg.cn/blog_migrate/6b4c6de15b10192ad67e608ef7f1463a.jpeg)
6.2 存在嵌套结构
当元组的数据中存在嵌套的可变类型,比如列表等,深拷贝会重新开辟地址,将元组重新成成一份。
![b5a4890cb9b237e13a2fa45b5e600f8a.png](https://i-blog.csdnimg.cn/blog_migrate/6c44155fb3b621734efbaaa36c5ef62e.jpeg)
七、is和==
在文章的开始就已经谈过:在Python中每个变量都有自己的标识、类型和值。每个对象一旦创建,它的标识就绝对不会变。一个对象的标识,我们可以理解成其在内存中的地址。is()
运算符比较的是两个对象的标识;id()
方法返回的就是对象标识的整数表示。
总结:is()
比较对象的标识;==
运算符比较两个对象的值(对象中保存的数据)。在实际的编程中,我们更多关注的是值,而不是标识本身。
第一个例子:我们创建了两个不同的对象,只是它们的值刚好相同而已。
![4d4467e1d4ebf901c8ba8d31574735ba.png](https://i-blog.csdnimg.cn/blog_migrate/0c11f080eb5deb0c413ecc67190ddb98.jpeg)
![c6cce49f289bfb595996ff353cf85aab.png](https://i-blog.csdnimg.cn/blog_migrate/cdd3b3d7e806948899f541e1f7073b61.jpeg)
第二个例子:我们先创建了一个对象v3,然后将他赋值给另一个对象v4,其实它们就是相同的对象,所以标识(内存地址)是相同的,只是它们的名字不同而已。
![6444e699d138c5318147d53c2c5fd637.png](https://i-blog.csdnimg.cn/blog_migrate/d3bfc6444fcee32b028ff6b7a891fc3d.jpeg)
![912a0798b39e59046e963bbc9cda2452.png](https://i-blog.csdnimg.cn/blog_migrate/9c2657213f7f9abd0dd3a61d5f1d1484.jpeg)
总结
通过大量的例子,我们得出结论:
- 在不可变数据类型中,深浅拷贝都不会开辟新的内存空间,用的都是同一个内存地址。
- 在存在嵌套可变类型的数据时,深浅拷贝都会开辟新的一块内存空间;同时,不可变类型的值还是指向原来的值的地址。
不同的是:在嵌套可变类型中,浅拷贝只会拷贝最外层的数据,而深拷贝会拷贝所有层级的可变类型数据。
![2d8d1f47721ffb002aa9eca382bbbf4b.png](https://i-blog.csdnimg.cn/blog_migrate/1f4583b44259d70afbcb133014fb76ab.png)