【Python基础】Python的深浅拷贝讲解
共 3881字,需浏览 8分钟
· 2021-11-11
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
在很多语言中都存在深浅拷贝两种拷贝数据的方式,Python中也不例外。本文中详细介绍了Python中的深浅拷贝的相关知识,文章的内容包含:
对象、数据类型、引用 赋值 浅拷贝 深拷贝
![](https://filescdn.proginn.com/f38d42b071442e425224b249b87bca69/ec208ce810655f8f34b92b4105b8687f.webp)
我们经常听到:在Python中一切皆对象。其实,说的就是我们在Python中构造的任何数据类型都是一个对象,不管是数字、字符串、字典等常见的数据结构,还是函数,甚至是我们导入的模块等,Python都会把它当做是一个对象来处理。
所有的Python对象都拥有3个属性:
身份 类型 值
我们看一个简单的例子来理解上面的3个属性:
假设我们声明了一个name变量,通过id、type方法能够查看对象的身份和类型:
![](https://filescdn.proginn.com/e13ac958efd999b69939913e7bae280b/44575c52fe1594183b8dcd5eca5982b0.webp)
甚至是type本身也是一个对象,它也拥有自己的身份、类型:
![](https://filescdn.proginn.com/a7bd71ec947775370ca652657bf6af91/fa6690756be99d863c47de9e2f5cf7eb.webp)
Python中,万物皆对象
2.1 可变和不可变类型
在Python中,按照更新对象的方式,我们可以将对象分为2大类:可变数据类型和不可变数据类型。
不可变数据类型:数值、字符串、布尔值。不可变对象就是对象的身份和值都不可变。新创建的对象被关联到原来的变量名,旧对象被丢弃,垃圾回收器会在适当的时机回收这些对象。
可变数据类型:列表、字典、集合。所谓的可变指的是可变对象的值可变,但是身份是不可变的。
首先我们看看不可变对象:
![](https://filescdn.proginn.com/06a042045eb0549806214f5f897cb569/803fbe91556935f6e03e83f536cb2715.webp)
当我们定义了一个对象str1,给其赋值了“python”,便会在内存中找到一个固定的内存地址来存放;但是,当我们将“python”定义成另一个变量名的时候,我们发现:它在内存中的位置是不变的。
![](https://filescdn.proginn.com/39685acd6622087a55205fe83d5b4423/a3445f065d4bd768a4e59463d952173a.webp)
也就是说,这个变量在计算机内存中的位置是不变的,只是换了一个名字来存放,来看3个实际的例子:
![](https://filescdn.proginn.com/2cc9150b7ea91a5a87a42ac11e41c850/4abd4ec831501db614924fdc0d98362c.webp)
![](https://filescdn.proginn.com/23144d01c3a49ec0584ddd38da2c99bb/382f764d6c3df58c7699877000711fa5.webp)
![](https://filescdn.proginn.com/df73827f635e64b0e92759017cb19f6d/dbf51d1df9f96568b3f39257ae438110.webp)
以上的例子说明:当我们对字符串、数值型、布尔值的数据改变变量名,并不会影响到数据在内存中的位置。
我们看看可变类型的例子,列表、字典、集合都是一样的效果:
![](https://filescdn.proginn.com/ffebe76f692466cb48da408095a8542f/fb1bb069d485c69d770ab27920d2746a.webp)
![](https://filescdn.proginn.com/927489c386682153b2a700f2a48248e4/67e432dadc198a1cee509cd661db7422.webp)
![](https://filescdn.proginn.com/04e95cbd8fc99af9390f3728c6e5df85/903384abf4f5c16366414fe77e774d0e.webp)
虽然是相同的数据,但是变量名字不同,内存中仍然会开辟新的内存地址来进行存放相同的数据,我们以字典为例:
![](https://filescdn.proginn.com/84f22ceefa0fba5fe271b3387d1f0d6f/a69d9acef639618bd30124c7a9c9c44c.webp)
2.2 引用
在Python语言中,每个对象都会在内存中申请开辟一块新的空间来保存对象;对象在内存中所在位置的地址称之为引用。
可以说,我们定义的变量名实际上就是对象的地址引用。引用实际上就是内存中的一个数字地址编号。在使用对象的时候,只要知道这个对象的地址,我们就可以操作这个对象。
因为这个数字地址不太容易记忆,所以我们使用变量名的形式来代替对象的数字地址。在Python中,变量就是地址的一种表示形式,并不会开辟新的存储空间。
我们通过一个例子来说明变量和变量指向的引用(内存地址)实际上就是一个东西:
![](https://filescdn.proginn.com/bc04cbaf46322f63228dfd0f7dc8526b/7157ffc4e1d286811b5e133c07c74bfb.webp)
![](https://filescdn.proginn.com/3dc8a596bfee91f248df6764a75e9fcf/2dac3079a2af31de82e847f4c389838d.webp)
3.1 相同数据,不同变量名
讨论完Python的对象、属性和引用3个重要的概念之后,在正式介绍深浅拷贝之前,我们先讨论Python中的赋值。
在Python中,每次赋值都会开辟新的内存地址来存放数据,比如我们同时存放一个列表[1,2,3],即使数据是相同的,但是内存地址却不同:
![](https://filescdn.proginn.com/281c4dede9c0241509c7d6e3b1344063/6190b57d943d519f721a5c249ce2333f.webp)
其实就是两个不同的变量,只是恰好它们存放了相同的数据而已,但是存放的地址是不同的。
![](https://filescdn.proginn.com/96bc682499365c860b96be011d604928/0f5438700c3421431b158c01e0c93971.webp)
我们给v1列表追加了一个元素,发现它的内存地址是不变的,当然v2肯定是不变的:
![](https://filescdn.proginn.com/7c060767da91ff58cd104ee0fef27750/042a17f00be2875da72bc96ad51db2e1.webp)
![](https://filescdn.proginn.com/ff08fd273efb572a0998bf420893a0f8/9d2ba1464966fe9b87e35584d4c35d76.webp)
3.2 一个变量多次赋值
如果我们对一个变量多次赋值,其内存是会变化的:
![](https://filescdn.proginn.com/abc831ee9e7dac37598c06f75c5352d4/c03c090fd31c429db8a4086df53b803d.webp)
![](https://filescdn.proginn.com/cf9c7580d95614a67a9f8229a975afc7/b09496101f9265f01bb659cf53acdaee.webp)
3.3 变量赋值
将一个变量赋值给另一个变量,其实它们就是同一个对象:数据相同,在内存中的地址也相同:
![](https://filescdn.proginn.com/4fa53e91ea002f0d5c68354e773f5155/67290f18bbfb72b200b348c5adddd128.webp)
![](https://filescdn.proginn.com/2bb29daa173b1c7260a214f9a80077c2/6c2a13bbbb9e8b4234fc24d2ef768d20.webp)
当我们给V1追加一个元素,V2也会同时变化:
![](https://filescdn.proginn.com/0af71b7d578644a75e704003caec4aa9/34de14645067cbd124aed11d641308ce.webp)
实际上它们就是同一个对象!!!!
3.4 嵌套赋值
如果是列表中嵌套着另外的列表,那么当改变其中一个列表的时候,另一个列表中的也会随着改变:
![](https://filescdn.proginn.com/9ce2b753c97858f344d905e5182f8ef5/d106199c32be506fefbea0ded8fccbe4.webp)
原始数据信息:
![](https://filescdn.proginn.com/8815e9fa3941c9e6299f4ff6ec9c66e5/e17a29e22dc01e1f88e290f0a20a2497.webp)
当我们给v1追加了新元素之后:
![](https://filescdn.proginn.com/ae65932f7b175ea6279c2666445b237e/20ad83fbb834d375b46169c852de337d.webp)
总结:赋值其实就是将一个对象的地址赋值给一个变量,使得变量指向该内存地址。
在Python中进行拷贝之前,我们需要导入模块:
import copy
⚠️浅拷贝只是拷贝数据的第一层,不会拷贝子对象。
4.1 不可变类型的浅拷贝
如果只是针对不可变的数据类型(字符串、数值型、布尔值),浅拷贝的对象和原数据对象是相同的内存地址:
![](https://filescdn.proginn.com/e6ecc322d7e707f6ed6f204c7da03b5d/031ca4a52f6a5474e9f91c5e6e5be0fa.webp)
![](https://filescdn.proginn.com/e049ce1cd434a67eea1c02f110357a55/11508a9cce8f551ba14c6bd0a32ccd60.webp)
从上面的结果中我们可以看出来:针对不可变类型的浅拷贝,只是换了一个名字,对象在内存中的地址其实是不变的。
![](https://filescdn.proginn.com/f5bb1241d8e117982524a5195d656fb8/5a5b37ed9128fda251a789a7c13c066c.webp)
4.2 可变类型的浅拷贝
首先我们讨论的是不存在嵌套类型的可变类型数据(列表、字典、集合):
![](https://filescdn.proginn.com/148c7552764258f931aec88ddd86ee55/5ef0b4fc806c7ac825207076b380e162.webp)
从上面的例子看出来:
列表本身的浅拷贝对象的地址和原对象的地址是不同的,因为列表是可变数据类型。 列表中的元素(第1个元素为例)和浅拷贝对象中的第一个元素的地址是相同的,因为元素本身是数值型,是不可变的。
通过一个图形来说明这个关系:
![](https://filescdn.proginn.com/c73c0ed64f731a7612a2c1330e9297be/31a81d70d7b468e32c97ad8fe9eddd2f.webp)
字典中也存在相同的情况:字典本身的内存地址不同,但是里面的键、值的内存地址是相同的,因为键值都是不可变类型的数据。
![](https://filescdn.proginn.com/ef564431f7e8e544a036dd98af7e4a9b/8c017a066920b1b358970f3141c23d29.webp)
如果可变类型的数据中存在嵌套的结构:
![](https://filescdn.proginn.com/1235319410cd18ceeca44a3b8f50e2a4/baee30be9b971450eb7caa06049c279b.webp)
从上面的两个例子中我们可以看出来:
在可变类型的数据中,如果存在嵌套的结构类型,浅拷贝只复制最外层的数据,导致内存地址发生变化,里面数据的内存地址不会变
深拷贝不同于浅拷贝的是:深拷贝会拷贝所有的可变数据类型,包含嵌套的数据中的可变数据。深拷贝是变量对应的值复制到新的内存地址中,而不是复制数据对应的内存地址。
5.1 不可变类型的深拷贝
关于不可变类型的深浅拷贝,其效果是相同的,具体看下面的例子:
![](https://filescdn.proginn.com/e8b321754a27d6569a0d857478ddb2b0/d2339b9c566f7d4a40cf91082391e35a.webp)
![](https://filescdn.proginn.com/df7dd3cd79c8a0fcfdee52a8f1cafc58/1347fc60b5bf6432ed140d43e7c1c9f8.webp)
![](https://filescdn.proginn.com/d82cd163cf75ef40e7850b20bd7843ef/939b6e0d4392337cffb70bd00fe8e5c5.webp)
我们得出一个结论:针对不可变数据类型的深浅拷贝,其结果是相同的。
5.2 可变类型的深拷贝
首先我们讨论的是不存在嵌套的情况:
针对列表数据:
![](https://filescdn.proginn.com/28204c4c15f5b29d35aee7a805b790b5/8da1fbf124e10fd38b820b702b712f11.webp)
![](https://filescdn.proginn.com/6ee996a30076b13b3bf46aec30973045/62c8a05d58113b46c92afc73ae9fc9b9.webp)
针对字典数据:
![](https://filescdn.proginn.com/67590f4f8ffb94b4caf6864414263f21/ab091bc392a591e57945653f8f936230.webp)
![](https://filescdn.proginn.com/49eb6fc9586439ce13f3a2115e30bd39/721eec8096694173654b2f4bbc76ed58.webp)
我们可以得出结论:
深拷贝对最外层数据是只拷贝数据,会开辟新的内存地址来存放数据。 深拷贝对里面的不可变数据类型直接复制数据和地址,和可变类型的浅拷贝是相同的效果。
![](https://filescdn.proginn.com/838e92aedf4e4cb9a96ee301492ae4d7/4e980c8fff77e07b5671754ac0c54abb.webp)
我们讨论存在嵌套类型的深拷贝(以列表为例)。
![](https://filescdn.proginn.com/96c11f7727f05f732d8851689d35c863/b1c2cb0fe844dd3699cc3636725b64c6.webp)
结论1:对整个存在嵌套类型的数据进行深浅拷贝都会发生内存的变化,因为数据本身是可变的。
![](https://filescdn.proginn.com/29d2bc32081088e1590585fbcc6fcf99/021cdc88078322d8ae62092dab6733b8.webp)
结论2:我们查看第一个元素1的内存地址,发生三者是相同的,因为1是属于数值型,是不可变类型。
![](https://filescdn.proginn.com/48cf3e831f846ecdc4f9f672b97ba57c/a959b341ee362fd8837b8425c95ea026.webp)
结论3:我们查看第三个元素即里面嵌套列表的内存,发现只有深拷贝是不同的,因为这个嵌套的列表是可变数据类型,深拷贝在拷贝了最外层之后还会继续拷贝子层级的可变类型。
![](https://filescdn.proginn.com/487f986eb6a26903bf59e59649d1db9d/d232ba9a0e5dd752e55fa0c229c80edf.webp)
结论4:我们查看嵌套列表中的元素的内存地址,发现它们是相同的,因为元素是数值型,是不可变的,不受拷贝的影响。
元组本身是不可变数据类型,但是其中的值是可以改变的,内部可以有嵌套可变数据类型,比如列表等,会对它的拷贝结果造成影响。
6.1 不存在嵌套结构
当元组中不存在嵌套结构的时候,元组的深浅拷贝是相同的效果:
![](https://filescdn.proginn.com/299bdf68b05d1350f4d303086b286131/cbf4c7b1b24562120ddcc76110884626.webp)
6.2 存在嵌套结构
当元组的数据中存在嵌套的可变类型,比如列表等,深拷贝会重新开辟地址,将元组重新成成一份。
![](https://filescdn.proginn.com/85506f7f0dd47e73b40fb31a25e8871c/67026c2668262a6efe34635d72d540b6.webp)
在文章的开始就已经谈过:在Python中每个变量都有自己的标识、类型和值。每个对象一旦创建,它的标识就绝对不会变。一个对象的标识,我们可以理解成其在内存中的地址。is()
运算符比较的是两个对象的标识;id()
方法返回的就是对象标识的整数表示。
总结:is()
比较对象的标识;==
运算符比较两个对象的值(对象中保存的数据)。在实际的编程中,我们更多关注的是值,而不是标识本身。
第一个例子:我们创建了两个不同的对象,只是它们的值刚好相同而已。
![](https://filescdn.proginn.com/17cbd16d69b8e8ea179af2203b63a845/ce7f0ee3c9b9d64f20842a437bc1ee98.webp)
![](https://filescdn.proginn.com/094696e93ec1880af0795f79ba9bd408/95cddbd01ec9a073aaecf7c6b30746f3.webp)
第二个例子:我们先创建了一个对象v3,然后将他赋值给另一个对象v4,其实它们就是相同的对象,所以标识(内存地址)是相同的,只是它们的名字不同而已。
![](https://filescdn.proginn.com/98d2446ba0e05f6647efa932ccbbf7f8/f42561141bc5067ab420c471ae1977ea.webp)
![](https://filescdn.proginn.com/44691c413e9ba1b6519a9cd139424eb2/31e1e874f07438fb67c68b5492d9a0c1.webp)
通过大量的例子,我们得出结论:
在不可变数据类型中,深浅拷贝都不会开辟新的内存空间,用的都是同一个内存地址。 在存在嵌套可变类型的数据时,深浅拷贝都会开辟新的一块内存空间;同时,不可变类型的值还是指向原来的值的地址。
不同的是:在嵌套可变类型中,浅拷贝只会拷贝最外层的数据,而深拷贝会拷贝所有层级的可变类型数据。
下载1:OpenCV-Contrib扩展模块中文版教程 在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。 下载2:Python视觉实战项目52讲 在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。 下载3:OpenCV实战项目20讲 在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。 交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~