平时的工作中,经常会涉及到数据传输。在数据传输过程中,数据可能会被修改。为了防止数据被修改,需要传递一个副本,即使修改了副本,也不会影响原始数据的使用。为了制作这个副本,制作了一个副本。今天就来聊聊Python中的深拷贝和浅拷贝。概念普及:对象、变量类型、引用数据拷贝会涉及到Python中对象、变量类型、引用这三个概念。我们先来看看这些概念。只有了解了它们,才能更好的理解深拷贝和浅拷贝是什么鬼。Python对象在Python中,对于对象有一句很流行的说法,万物皆对象。据说任何构造的数据类型都是对象,无论是数字、字符串、函数,甚至是模块,Python都将其视为对象。所有Python对象都具有三个属性:标识、类型和值。看一个简单的例子:In[1]:name="laowang"#nameobjectIn[2]:id(name)#id:身份的唯一标识Out[2]:1698668550104In[3]:type(name)#type:对象的类型,决定了对象可以持有什么类型的值。Out[3]:strIn[4]:name#对象的值,代表数据Out[4]:'laowang'可变和不可变对象在Python中,根据对象更新的方式,对象可以是分为两类:可变对象和不可变对象。可变对象:列表、字典、集合所谓可变,就是可变对象的值可以改变,但身份不变。不可变对象:数字、字符串、元组不可变对象是其标识和值无法更改的对象。新创建的对象与原来的变量名相关联,旧的对象被丢弃,垃圾收集器在适当的时候回收这些对象。In[7]:var1="python"In[8]:id(var1)Out[8]:1700782038408#因为var1不可变,重新创建java对象,id改变,旧对象python会在一个某些In[9]:var1="java"In[10]:id(var1)Out[10]:1700767578296Reference在Python程序中,每个对象都会在内存中申请一块空间来保存对象,其中的地址位于内存中的对象称为引用。在开发程序时,定义的变量名实际上是指对象的地址。引用实际上是内存中的数字地址号。在使用对象时,只要知道对象的地址,就可以对对象进行操作。但是由于这个数字地址在开发中不方便使用和记忆,所以采用了变量名的形式。替换对象的数字地址。在Python中,变量是地址的表示,不开辟存储空间。就像IP地址一样,在访问一个网站时,主机实际上是由IP地址决定的,而IP地址不好记,所以用域名代替IP地址。使用域名访问网站时,将域名解析成IP地址使用。用一个例子来说明变量和变量的引用是一回事。In[11]:age=18In[12]:id(age)Out[12]:1730306752In[13]:id(18)Out[13]:1730306752一步一步:参考赋值上面已经理解了,参考是对象在内存中的数值地址编号,变量的出现是为了方便引用的表示,变量指向这个引用。赋值的本质是让多个变量同时引用同一个对象的地址。那么当数据被修改时会发生什么?分配对不可变对象的引用。给不可变对象赋值,其实就是在内存中开辟一块空间指向新的对象,原来的不可变对象不会被修改。示意图如下:通过一个案例来理解:a和b在内存中都是对1的引用,所以a和b的引用是一样的。In[1]:a=1In[2]:b=aIn[3]:id(a)Out[3]:1730306496In[4]:id(b)Out[4]:1730306496现在重新分配a,看看会发生什么改变?从下面不难看出:给a赋新对象时,会指向当前引用,而不是旧对象引用。In[1]:a=1In[2]:b=aIn[5]:a=2In[6]:id(a)Out[6]:1730306816In[7]:id(b)Out[7]:1730306496可变对象的引用分配。可变对象不存储真实的对象数据,而是对象引用。为可变对象赋值时,只是将可变对象中保存的引用指向新对象。示意图如下:还是用一个例子来体验变量对象引用赋值的过程。当l1改变时,整个列表的引用都会指向新的对象,但是l1和l2都是对同一个保存列表的引用,所以引用地址不会改变。In[3]:l1=[1,2,3]In[4]:l2=l1In[5]:id(l1)Out[5]:1916633584008In[6]:id(l2)Out[6]:1916633584008In[7]:l1[0]=11In[8]:id(l1)Out[8]:1916633584008In[9]:id(l2)Out[9]:1916633584008题目详解:浅拷贝,深拷贝后前两部分大家应该对对象的引用赋值有一个清晰的认识。我们来思考这样一个问题:如何解决Python中函数传递后不影响原始数据的问题?这个问题已经被Python帮我们解决了,使用对象拷贝或者深拷贝都可以愉快的解决。下面我们就来看看浅拷贝和深拷贝在Python中是如何实现的。浅拷贝:为了解决函数传递后被修改的问题,需要复制一个副本,并将副本传递给函数使用。即使副本被修改,原始数据也不会受到影响。不可变对象的复制不可变对象只有在被修改时才会在内存中开辟新的空间,而复制实际上是让多个对象同时指向一个引用,这和对象赋值没什么区别。同样,通过一个例子感受一下:不难看出a和b指向同一个引用,不可变对象的拷贝就是对象赋值。In[11]:importcopyIn[12]:a=10In[13]:b=copy.copy(a)In[14]:id(a)Out[14]:1730306496In[15]:id(b)Out[15]:1730306496可变对象的副本对于不可变对象的副本,对象的引用并没有改变,那么可变对象的副本会和不可变对象一样吗?我们往下看。从下面的例子可以看出:一个可变对象的拷贝会在内存中开辟一个新的空间来保存拷贝的数据。更改前一个对象时,它对复制的对象没有影响。In[24]:importcopyIn[25]:l1=[1,2,3]In[26]:l2=copy.copy(l1)In[27]:id(l1)Out[27]:1916631742088In[28]原理图如下:现在我们回到刚才的问题。浅拷贝能解决函数传完后原数据不变的问题吗?让我们看一个稍微复杂一点的数据结构。通过下面的例子可以发现,复制复杂对象时,并没有解决数据传输后数据变化的问题。这样做的原因是,copy()函数在复制一个对象时,只是复制了指定对象中的所有引用。如果这些引用包含一个可变对象,数据仍然会被改变。这种复制方法称为浅复制。In[35]:a=[1,2]In[36]:l1=[3,4,a]In[37]:l2=copy.copy(l1)In[38]:id(l1)Out[38]:1916631704520In[39]:id(l2)Out[39]:1916631713736In[40]:a[0]=11In[41]:id(l1)Out[41]:1916631704520In[42]:id(l2)Out[42]:1916631713736In[43]:l1Out[43]:[3,4,[11,2]]In[44]:l2Out[44]:[3,4,[11,2]]示意图如下:针对上述情况,Python还提供了另一种拷贝方式(深拷贝)来解决。与浅拷贝不同,深拷贝只拷贝顶层引用。深拷贝会逐层拷贝,直到所有拷贝的引用都是不可变引用。接下来我们看看如果上面的复制例子使用了深复制,原始数据改变的问题还会存在吗?下面的例子很明确的告诉我们:前面的问题完全可以解决。importcopyl1=[3,4,a]In[47]:l2=copy.deepcopy(li)In[48]:id(l1)Out[48]:1916632194312In[49]:id(l2)Out[49]:1916634281416In[50]:a[0]=11In[51]:id(l1)Out[51]:1916632194312In[52]:id(l2)Out[52]:1916634281416In[54]:l1Out[54]:[3,4,[11,2]]In[55]:l2Out[55]:[1,2,3]示意图如下:查漏补缺。为什么Python默认的拷贝方式是浅拷贝?时间观点:浅拷贝花费的时间更少。空间视角:浅拷贝占用内存少。从效率的角度:浅拷贝只拷贝顶层数据,一般比深拷贝效率更高。本文知识点总结:不可变对象在赋值的时候会开辟新的空间。当一个可变对象被赋予一个值时,修改其中一个的值将改变另一个。深拷贝和浅拷贝复制不可变对象时,并没有开辟新的空间,相当于一个赋值操作。复制浅拷贝时,只复制顶层的引用。如果元素是一个可变对象并且被修改,复制的对象也会改变。深拷贝时,会一层层拷贝,直到所有的引用都是不可变对象。Python中浅拷贝的实现方式有很多种,copy模块的copy函数,对象的copy函数,工厂方法,slice等。大多数情况下,在写程序的时候,除非有特定的需要,都会使用浅拷贝。浅拷贝的优点:拷贝速度快,占用空间少,拷贝效率高。
