【Python基礎(chǔ)】Python的深淺拷貝講解
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時間送達(dá)
在很多語言中都存在深淺拷貝兩種拷貝數(shù)據(jù)的方式,Python中也不例外。本文中詳細(xì)介紹了Python中的深淺拷貝的相關(guān)知識,文章的內(nèi)容包含:
對象、數(shù)據(jù)類型、引用 賦值 淺拷貝 深拷貝

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

甚至是type本身也是一個對象,它也擁有自己的身份、類型:

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

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

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



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



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

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


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

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

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


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


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


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

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

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

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

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


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

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

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

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

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

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



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


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


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

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

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

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

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

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

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

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


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


通過大量的例子,我們得出結(jié)論:
在不可變數(shù)據(jù)類型中,深淺拷貝都不會開辟新的內(nèi)存空間,用的都是同一個內(nèi)存地址。 在存在嵌套可變類型的數(shù)據(jù)時,深淺拷貝都會開辟新的一塊內(nèi)存空間;同時,不可變類型的值還是指向原來的值的地址。
不同的是:在嵌套可變類型中,淺拷貝只會拷貝最外層的數(shù)據(jù),而深拷貝會拷貝所有層級的可變類型數(shù)據(jù)。
下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程 在「小白學(xué)視覺」公眾號后臺回復(fù):擴(kuò)展模塊中文教程,即可下載全網(wǎng)第一份OpenCV擴(kuò)展模塊教程中文版,涵蓋擴(kuò)展模塊安裝、SFM算法、立體視覺、目標(biāo)跟蹤、生物視覺、超分辨率處理等二十多章內(nèi)容。 下載2:Python視覺實(shí)戰(zhàn)項(xiàng)目52講 在「小白學(xué)視覺」公眾號后臺回復(fù):Python視覺實(shí)戰(zhàn)項(xiàng)目,即可下載包括圖像分割、口罩檢測、車道線檢測、車輛計(jì)數(shù)、添加眼線、車牌識別、字符識別、情緒檢測、文本內(nèi)容提取、面部識別等31個視覺實(shí)戰(zhàn)項(xiàng)目,助力快速學(xué)校計(jì)算機(jī)視覺。 下載3:OpenCV實(shí)戰(zhàn)項(xiàng)目20講 在「小白學(xué)視覺」公眾號后臺回復(fù):OpenCV實(shí)戰(zhàn)項(xiàng)目20講,即可下載含有20個基于OpenCV實(shí)現(xiàn)20個實(shí)戰(zhàn)項(xiàng)目,實(shí)現(xiàn)OpenCV學(xué)習(xí)進(jìn)階。 交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計(jì)算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三?+?上海交大?+?視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~
