【Python基礎(chǔ)】Python的深淺拷貝講解
前言
在很多語(yǔ)言中都存在深淺拷貝兩種拷貝數(shù)據(jù)的方式,Python中也不例外。本文中詳細(xì)介紹了Python中的深淺拷貝的相關(guān)知識(shí),文章的內(nèi)容包含:
對(duì)象、數(shù)據(jù)類型、引用 賦值 淺拷貝 深拷貝

一、Python對(duì)象
我們經(jīng)常聽(tīng)到:在Python中一切皆對(duì)象。其實(shí),說(shuō)的就是我們?cè)赑ython中構(gòu)造的任何數(shù)據(jù)類型都是一個(gè)對(duì)象,不管是數(shù)字、字符串、字典等常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),還是函數(shù),甚至是我們導(dǎo)入的模塊等,Python都會(huì)把它當(dāng)做是一個(gè)對(duì)象來(lái)處理。
所有的Python對(duì)象都擁有3個(gè)屬性:
身份 類型 值
我們看一個(gè)簡(jiǎn)單的例子來(lái)理解上面的3個(gè)屬性:
假設(shè)我們聲明了一個(gè)name變量,通過(guò)id、type方法能夠查看對(duì)象的身份和類型:

甚至是type本身也是一個(gè)對(duì)象,它也擁有自己的身份、類型:

Python中,萬(wàn)物皆對(duì)象
二、數(shù)據(jù)類型
2.1 可變和不可變類型
在Python中,按照更新對(duì)象的方式,我們可以將對(duì)象分為2大類:可變數(shù)據(jù)類型和不可變數(shù)據(jù)類型。
不可變數(shù)據(jù)類型:數(shù)值、字符串、布爾值。不可變對(duì)象就是對(duì)象的身份和值都不可變。新創(chuàng)建的對(duì)象被關(guān)聯(lián)到原來(lái)的變量名,舊對(duì)象被丟棄,垃圾回收器會(huì)在適當(dāng)?shù)臅r(shí)機(jī)回收這些對(duì)象。
可變數(shù)據(jù)類型:列表、字典、集合。所謂的可變指的是可變對(duì)象的值可變,但是身份是不可變的。
首先我們看看不可變對(duì)象:

當(dāng)我們定義了一個(gè)對(duì)象str1,給其賦值了“python”,便會(huì)在內(nèi)存中找到一個(gè)固定的內(nèi)存地址來(lái)存放;但是,當(dāng)我們將“python”定義成另一個(gè)變量名的時(shí)候,我們發(fā)現(xiàn):它在內(nèi)存中的位置是不變的。

也就是說(shuō),這個(gè)變量在計(jì)算機(jī)內(nèi)存中的位置是不變的,只是換了一個(gè)名字來(lái)存放,來(lái)看3個(gè)實(shí)際的例子:



以上的例子說(shuō)明:當(dāng)我們對(duì)字符串、數(shù)值型、布爾值的數(shù)據(jù)改變變量名,并不會(huì)影響到數(shù)據(jù)在內(nèi)存中的位置。
我們看看可變類型的例子,列表、字典、集合都是一樣的效果:



雖然是相同的數(shù)據(jù),但是變量名字不同,內(nèi)存中仍然會(huì)開(kāi)辟新的內(nèi)存地址來(lái)進(jìn)行存放相同的數(shù)據(jù),我們以字典為例:

2.2 引用
在Python語(yǔ)言中,每個(gè)對(duì)象都會(huì)在內(nèi)存中申請(qǐng)開(kāi)辟一塊新的空間來(lái)保存對(duì)象;對(duì)象在內(nèi)存中所在位置的地址稱之為引用。
可以說(shuō),我們定義的變量名實(shí)際上就是對(duì)象的地址引用。引用實(shí)際上就是內(nèi)存中的一個(gè)數(shù)字地址編號(hào)。在使用對(duì)象的時(shí)候,只要知道這個(gè)對(duì)象的地址,我們就可以操作這個(gè)對(duì)象。
因?yàn)檫@個(gè)數(shù)字地址不太容易記憶,所以我們使用變量名的形式來(lái)代替對(duì)象的數(shù)字地址。在Python中,變量就是地址的一種表示形式,并不會(huì)開(kāi)辟新的存儲(chǔ)空間。
我們通過(guò)一個(gè)例子來(lái)說(shuō)明變量和變量指向的引用(內(nèi)存地址)實(shí)際上就是一個(gè)東西:


三、賦值
3.1 相同數(shù)據(jù),不同變量名
討論完P(guān)ython的對(duì)象、屬性和引用3個(gè)重要的概念之后,在正式介紹深淺拷貝之前,我們先討論P(yáng)ython中的賦值。
在Python中,每次賦值都會(huì)開(kāi)辟新的內(nèi)存地址來(lái)存放數(shù)據(jù),比如我們同時(shí)存放一個(gè)列表[1,2,3],即使數(shù)據(jù)是相同的,但是內(nèi)存地址卻不同:

其實(shí)就是兩個(gè)不同的變量,只是恰好它們存放了相同的數(shù)據(jù)而已,但是存放的地址是不同的。

我們給v1列表追加了一個(gè)元素,發(fā)現(xiàn)它的內(nèi)存地址是不變的,當(dāng)然v2肯定是不變的:


3.2 一個(gè)變量多次賦值
如果我們對(duì)一個(gè)變量多次賦值,其內(nèi)存是會(huì)變化的:


3.3 變量賦值
將一個(gè)變量賦值給另一個(gè)變量,其實(shí)它們就是同一個(gè)對(duì)象:數(shù)據(jù)相同,在內(nèi)存中的地址也相同:


當(dāng)我們給V1追加一個(gè)元素,V2也會(huì)同時(shí)變化:

實(shí)際上它們就是同一個(gè)對(duì)象!!!!
3.4 嵌套賦值
如果是列表中嵌套著另外的列表,那么當(dāng)改變其中一個(gè)列表的時(shí)候,另一個(gè)列表中的也會(huì)隨著改變:

原始數(shù)據(jù)信息:

當(dāng)我們給v1追加了新元素之后:

總結(jié):賦值其實(shí)就是將一個(gè)對(duì)象的地址賦值給一個(gè)變量,使得變量指向該內(nèi)存地址。
四、淺拷貝
在Python中進(jìn)行拷貝之前,我們需要導(dǎo)入模塊:
import copy
??淺拷貝只是拷貝數(shù)據(jù)的第一層,不會(huì)拷貝子對(duì)象。
4.1 不可變類型的淺拷貝
如果只是針對(duì)不可變的數(shù)據(jù)類型(字符串、數(shù)值型、布爾值),淺拷貝的對(duì)象和原數(shù)據(jù)對(duì)象是相同的內(nèi)存地址:


從上面的結(jié)果中我們可以看出來(lái):針對(duì)不可變類型的淺拷貝,只是換了一個(gè)名字,對(duì)象在內(nèi)存中的地址其實(shí)是不變的。

4.2 可變類型的淺拷貝
首先我們討論的是不存在嵌套類型的可變類型數(shù)據(jù)(列表、字典、集合):

從上面的例子看出來(lái):
列表本身的淺拷貝對(duì)象的地址和原對(duì)象的地址是不同的,因?yàn)榱斜硎强勺償?shù)據(jù)類型。 列表中的元素(第1個(gè)元素為例)和淺拷貝對(duì)象中的第一個(gè)元素的地址是相同的,因?yàn)?strong style="font-weight: bold;line-height: 1.75em;color: rgb(60, 123, 100);">元素本身是數(shù)值型,是不可變的。
通過(guò)一個(gè)圖形來(lái)說(shuō)明這個(gè)關(guān)系:

字典中也存在相同的情況:字典本身的內(nèi)存地址不同,但是里面的鍵、值的內(nèi)存地址是相同的,因?yàn)殒I值都是不可變類型的數(shù)據(jù)。

如果可變類型的數(shù)據(jù)中存在嵌套的結(jié)構(gòu):

從上面的兩個(gè)例子中我們可以看出來(lái):
在可變類型的數(shù)據(jù)中,如果存在嵌套的結(jié)構(gòu)類型,淺拷貝只復(fù)制最外層的數(shù)據(jù),導(dǎo)致內(nèi)存地址發(fā)生變化,里面數(shù)據(jù)的內(nèi)存地址不會(huì)變。
五、深拷貝
深拷貝不同于淺拷貝的是:深拷貝會(huì)拷貝所有的可變數(shù)據(jù)類型,包含嵌套的數(shù)據(jù)中的可變數(shù)據(jù)。深拷貝是變量對(duì)應(yīng)的值復(fù)制到新的內(nèi)存地址中,而不是復(fù)制數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址。
5.1 不可變類型的深拷貝
關(guān)于不可變類型的深淺拷貝,其效果是相同的,具體看下面的例子:



我們得出一個(gè)結(jié)論:針對(duì)不可變數(shù)據(jù)類型的深淺拷貝,其結(jié)果是相同的。
5.2 可變類型的深拷貝
首先我們討論的是不存在嵌套的情況:
針對(duì)列表數(shù)據(jù):


針對(duì)字典數(shù)據(jù):


我們可以得出結(jié)論:
深拷貝對(duì)最外層數(shù)據(jù)是只拷貝數(shù)據(jù),會(huì)開(kāi)辟新的內(nèi)存地址來(lái)存放數(shù)據(jù)。 深拷貝對(duì)里面的不可變數(shù)據(jù)類型直接復(fù)制數(shù)據(jù)和地址,和可變類型的淺拷貝是相同的效果。

我們討論存在嵌套類型的深拷貝(以列表為例)。

結(jié)論1:對(duì)整個(gè)存在嵌套類型的數(shù)據(jù)進(jìn)行深淺拷貝都會(huì)發(fā)生內(nèi)存的變化,因?yàn)閿?shù)據(jù)本身是可變的。

結(jié)論2:我們查看第一個(gè)元素1的內(nèi)存地址,發(fā)生三者是相同的,因?yàn)?是屬于數(shù)值型,是不可變類型。

結(jié)論3:我們查看第三個(gè)元素即里面嵌套列表的內(nèi)存,發(fā)現(xiàn)只有深拷貝是不同的,因?yàn)檫@個(gè)嵌套的列表是可變數(shù)據(jù)類型,深拷貝在拷貝了最外層之后還會(huì)繼續(xù)拷貝子層級(jí)的可變類型。

結(jié)論4:我們查看嵌套列表中的元素的內(nèi)存地址,發(fā)現(xiàn)它們是相同的,因?yàn)樵厥菙?shù)值型,是不可變的,不受拷貝的影響。
六、元組的深淺拷貝
元組本身是不可變數(shù)據(jù)類型,但是其中的值是可以改變的,內(nèi)部可以有嵌套可變數(shù)據(jù)類型,比如列表等,會(huì)對(duì)它的拷貝結(jié)果造成影響。
6.1 不存在嵌套結(jié)構(gòu)
當(dāng)元組中不存在嵌套結(jié)構(gòu)的時(shí)候,元組的深淺拷貝是相同的效果:

6.2 存在嵌套結(jié)構(gòu)
當(dāng)元組的數(shù)據(jù)中存在嵌套的可變類型,比如列表等,深拷貝會(huì)重新開(kāi)辟地址,將元組重新成成一份。

七、is和==
在文章的開(kāi)始就已經(jīng)談過(guò):在Python中每個(gè)變量都有自己的標(biāo)識(shí)、類型和值。每個(gè)對(duì)象一旦創(chuàng)建,它的標(biāo)識(shí)就絕對(duì)不會(huì)變。一個(gè)對(duì)象的標(biāo)識(shí),我們可以理解成其在內(nèi)存中的地址。is()運(yùn)算符比較的是兩個(gè)對(duì)象的標(biāo)識(shí);id()方法返回的就是對(duì)象標(biāo)識(shí)的整數(shù)表示。
總結(jié):is()比較對(duì)象的標(biāo)識(shí);==運(yùn)算符比較兩個(gè)對(duì)象的值(對(duì)象中保存的數(shù)據(jù))。在實(shí)際的編程中,我們更多關(guān)注的是值,而不是標(biāo)識(shí)本身。
第一個(gè)例子:我們創(chuàng)建了兩個(gè)不同的對(duì)象,只是它們的值剛好相同而已。


第二個(gè)例子:我們先創(chuàng)建了一個(gè)對(duì)象v3,然后將他賦值給另一個(gè)對(duì)象v4,其實(shí)它們就是相同的對(duì)象,所以標(biāo)識(shí)(內(nèi)存地址)是相同的,只是它們的名字不同而已。


總結(jié)
通過(guò)大量的例子,我們得出結(jié)論:
在不可變數(shù)據(jù)類型中,深淺拷貝都不會(huì)開(kāi)辟新的內(nèi)存空間,用的都是同一個(gè)內(nèi)存地址。 在存在嵌套可變類型的數(shù)據(jù)時(shí),深淺拷貝都會(huì)開(kāi)辟新的一塊內(nèi)存空間;同時(shí),不可變類型的值還是指向原來(lái)的值的地址。
不同的是:在嵌套可變類型中,淺拷貝只會(huì)拷貝最外層的數(shù)據(jù),而深拷貝會(huì)拷貝所有層級(jí)的可變類型數(shù)據(jù)。
往期精彩回顧
獲取本站知識(shí)星球優(yōu)惠券,復(fù)制鏈接直接打開(kāi):
https://t.zsxq.com/qFiUFMV
本站qq群704220115。
加入微信群請(qǐng)掃碼:
