Illustrations by Leon Tukker
作者:PayneLi,Python全家桶,主要講述數(shù)據(jù)挖掘、機(jī)器學(xué)習(xí)和深度學(xué)習(xí)領(lǐng)域的前沿技術(shù),同時(shí)還會(huì)推薦一些行業(yè)最新論文、技術(shù)專家的經(jīng)驗(yàn)分享。
在平時(shí)工作中,經(jīng)常涉及到數(shù)據(jù)的傳遞,在數(shù)據(jù)傳遞使用過(guò)程中,可能會(huì)發(fā)生數(shù)據(jù)被修改的問(wèn)題。為了防止數(shù)據(jù)被修改,就需要在傳遞一個(gè)副本,即使副本被修改,也不會(huì)影響原數(shù)據(jù)的使用。為了生成這個(gè)副本,就產(chǎn)生了拷貝。今天就說(shuō)一下Python中的深拷貝與淺拷貝的問(wèn)題。
概念普及:對(duì)象、可變類型、引用
數(shù)據(jù)拷貝會(huì)涉及到Python中對(duì)象、可變類型、引用這3個(gè)概念,先來(lái)看看這幾個(gè)概念,只有明白了他們才能更好的理解深拷貝與淺拷貝到底是怎么一回事。
在Python中,對(duì)對(duì)象有一種很通俗的說(shuō)法,萬(wàn)物皆對(duì)象。說(shuō)的就是構(gòu)造的任何數(shù)據(jù)類型都是一個(gè)對(duì)象,無(wú)論是數(shù)字,字符串,還是函數(shù),甚至是模塊,Python都對(duì)當(dāng)做對(duì)象處理。
所有Python對(duì)象都擁有三個(gè)屬性:身份、類型、值。
看一個(gè)簡(jiǎn)單的例子:
In [1]: name = 'laowang' # name對(duì)象
In [2]: id(name) # id:身份的唯一標(biāo)識(shí)
Out[2]: 1698668550104
In [3]: type(name) # type:對(duì)象的類型,決定了該對(duì)象可以保存什么類型的值
Out[3]: str
In [4]: name # 對(duì)象的值,表示的數(shù)據(jù)
Out[4]: 'laowang'
在Python中,按更新對(duì)象的方式,可以將對(duì)象分為2大類:可變對(duì)象與不可變對(duì)象。
可變對(duì)象: 列表、字典、集合
所謂可變是指可變對(duì)象的值可變,身份是不變的。
不可變對(duì)象:數(shù)字、字符串、元組
不可變對(duì)象就是對(duì)象的身份和值都不可變。新創(chuàng)建的對(duì)象被關(guān)聯(lián)到原來(lái)的變量名,舊對(duì)象被丟棄,垃圾回收器會(huì)在適當(dāng)?shù)臅r(shí)機(jī)回收這些對(duì)象。
In [7]: var1 = 'python'
In [8]: id(var1)
Out[8]: 1700782038408
#由于var1是不可變的,重新創(chuàng)建了java對(duì)象,隨之id改變,舊對(duì)象python會(huì)在某個(gè)時(shí)刻被回收
In [9]: var1 = 'java'
In [10]: id(var1)
Out[10]: 1700767578296
在 Python 程序中,每個(gè)對(duì)象都會(huì)在內(nèi)存中申請(qǐng)開(kāi)辟一塊空間來(lái)保存該對(duì)象,該對(duì)象在內(nèi)存中所在位置的地址被稱為引用。在開(kāi)發(fā)程序時(shí),所定義的變量名實(shí)際就對(duì)象的地址引用。
引用實(shí)際就是內(nèi)存中的一個(gè)數(shù)字地址編號(hào),在使用對(duì)象時(shí),只要知道這個(gè)對(duì)象的地址,就可以操作這個(gè)對(duì)象,但是因?yàn)檫@個(gè)數(shù)字地址不方便在開(kāi)發(fā)時(shí)使用和記憶,所以使用變量名的形式來(lái)代替對(duì)象的數(shù)字地址。 在 Python 中,變量就是地址的一種表示形式,并不開(kāi)辟開(kāi)辟存儲(chǔ)空間。
就像 IP 地址,在訪問(wèn)網(wǎng)站時(shí),實(shí)際都是通過(guò) IP 地址來(lái)確定主機(jī),而 IP 地址不方便記憶,所以使用域名來(lái)代替 IP 地址,在使用域名訪問(wèn)網(wǎng)站時(shí),域名被解析成 IP 地址來(lái)使用。
通過(guò)一個(gè)例子來(lái)說(shuō)明變量和變量指向的引用就是一個(gè)東西
In [11]: age = 18
In [12]: id(age)
Out[12]: 1730306752
In [13]: id(18)
Out[13]: 1730306752
逐步深入:引用賦值
上邊已經(jīng)明白,引用就是對(duì)象在內(nèi)存中的數(shù)字地址編號(hào),變量就是方便對(duì)引用的表示而出現(xiàn)的,變量指向的就是此引用。賦值的本質(zhì)就是讓多個(gè)變量同時(shí)引用同一個(gè)對(duì)象的地址。 那么在對(duì)數(shù)據(jù)修改時(shí)會(huì)發(fā)生什么問(wèn)題呢?
不可變對(duì)象的引用賦值
對(duì)不可變對(duì)象賦值,實(shí)際就是在內(nèi)存中開(kāi)辟一片空間指向新的對(duì)象,原不可變對(duì)象不會(huì)被修改。
原理圖如下:
下面通過(guò)案例來(lái)理解一下:
a與b在內(nèi)存中都是指向1的引用,所以a、b的引用是相同的
In [1]: a = 1
In [2]: b = a
In [3]: id(a)
Out[3]: 1730306496
In [4]: id(b)
Out[4]: 1730306496
現(xiàn)在再給a重新賦值,看看會(huì)發(fā)生什么變化?
從下面不難看出:當(dāng)給a 賦新的對(duì)象時(shí),將指向現(xiàn)在的引用,不在指向舊的對(duì)象引用。
In [1]: a = 1
In [2]: b = a
In [5]: a = 2
In [6]: id(a)
Out[6]: 1730306816
In [7]: id(b)
Out[7]: 1730306496
可變對(duì)象的引用賦值
可變對(duì)象保存的并不是真正的對(duì)象數(shù)據(jù),而是對(duì)象的引用。當(dāng)對(duì)可變對(duì)象進(jìn)行賦值時(shí),只是將可變對(duì)象中保存的引用指向了新的對(duì)象。
原理圖如下:
仍然通過(guò)一個(gè)實(shí)例來(lái)體會(huì)一下,可變對(duì)象引用賦值的過(guò)程。
當(dāng)改變l1時(shí),整個(gè)列表的引用會(huì)指新的對(duì)象,但是l1與l2都是指向保存的同一個(gè)列表的引用,所以引用地址不會(huì)變。
In [3]: l1 = [1, 2, 3]
In [4]: l2 = l1
In [5]: id(l1)
Out[5]: 1916633584008
In [6]: id(l2)
Out[6]: 1916633584008
In [7]: l1[0] = 11
In [8]: id(l1)
Out[8]: 1916633584008
In [9]: id(l2)
Out[9]: 1916633584008
主旨詳解:淺拷貝、深拷貝
經(jīng)過(guò)前2部分的解讀,大家對(duì)對(duì)象的引用賦值應(yīng)該有了一個(gè)清晰的認(rèn)識(shí)了。
下面大家思考一個(gè)這樣的問(wèn)題:Python中如何解決原始數(shù)據(jù)在函數(shù)傳遞之后不受影響了?
這個(gè)問(wèn)題Python已經(jīng)幫我們解決了,使用對(duì)象的拷貝或者深拷貝就可以愉快的解決了。
下面具體來(lái)看看Python中的淺拷貝與深拷貝是如何實(shí)現(xiàn)的。
淺拷貝:
為了解決函數(shù)傳遞后被修改的問(wèn)題,就需要拷貝一份副本,將副本傳遞給函數(shù)使用,就算是副本被修改,也不會(huì)影響原始數(shù)據(jù) 。
不可變對(duì)象只在修改的時(shí)候才會(huì)在內(nèi)存中開(kāi)辟新的空間, 而拷貝實(shí)際上是讓多個(gè)對(duì)象同時(shí)指向一個(gè)引用,和對(duì)象的賦值沒(méi)區(qū)別。
同樣的,通過(guò)一個(gè)實(shí)例來(lái)感受一下:不難看出,a與b指向相同的引用,不可變對(duì)象的拷貝就是對(duì)象賦值。
In [11]: import copy
In [12]: a = 10
In [13]: b = copy.copy(a)
In [14]: id(a)
Out[14]: 1730306496
In [15]: id(b)
Out[15]: 1730306496
對(duì)于不可變對(duì)象的拷貝,對(duì)象的引用并沒(méi)有發(fā)生變化,那么可變對(duì)象的拷貝會(huì)不會(huì)和不可變對(duì)象一樣了?我們接著往下看。
通過(guò)下面這個(gè)實(shí)例可以看出:可變對(duì)象的拷貝,會(huì)在內(nèi)存中開(kāi)辟一個(gè)新的空間來(lái)保存拷貝的數(shù)據(jù)。當(dāng)再改變之前的對(duì)象時(shí),對(duì)拷貝之后的對(duì)象沒(méi)有任何影響。
In [24]: import copy
In [25]: l1 = [1, 2, 3]
In [26]: l2 = copy.copy(l1)
In [27]: id(l1)
Out[27]: 1916631742088
In [28]: id(l2)
Out[28]: 1916636282952
In [29]: l1[0] = 11
In [30]: id(l1)
Out[30]: 1916631742088
In [31]: id(l2)
Out[31]: 1916636282952
原理圖如下:
現(xiàn)在再回到剛才那個(gè)問(wèn)題,是不是淺拷貝就可以解決原始數(shù)據(jù)在函數(shù)傳遞之后不變的問(wèn)題了?下面看一個(gè)稍微復(fù)雜一點(diǎn)的數(shù)據(jù)結(jié)構(gòu)。
通過(guò)下面這個(gè)實(shí)例可以發(fā)現(xiàn):復(fù)雜對(duì)象在拷貝時(shí),并沒(méi)有解決數(shù)據(jù)在傳遞之后,數(shù)據(jù)改變的問(wèn)題。 出現(xiàn)這種原因,是copy() 函數(shù)在拷貝對(duì)象時(shí),只是將指定對(duì)象中的所有引用拷貝了一份,如果這些引用當(dāng)中包含了一個(gè)可變對(duì)象的話,那么數(shù)據(jù)還是會(huì)被改變。 這種拷貝方式,稱為淺拷貝。
In [35]: a = [1, 2]
In [36]: l1 = [3, 4, a]
In [37]: l2 = copy.copy(l1)
In [38]: id(l1)
Out[38]: 1916631704520
In [39]: id(l2)
Out[39]: 1916631713736
In [40]: a[0] = 11
In [41]: id(l1)
Out[41]: 1916631704520
In [42]: id(l2)
Out[42]: 1916631713736
In [43]: l1
Out[43]: [3, 4, [11, 2]]
In [44]: l2
Out[44]: [3, 4, [11, 2]]
原理圖如下:
對(duì)于上邊這種狀況,Python還提供了另一種拷貝方式(深拷貝)來(lái)解決。
深拷貝
區(qū)別于淺拷貝只拷貝頂層引用,深拷貝會(huì)逐層進(jìn)行拷貝,直到拷貝的所有引用都是不可變引用為止。
接下來(lái)我們看看,要是將上邊的拷貝實(shí)例用使用深拷貝的話,原始數(shù)據(jù)改變的問(wèn)題還會(huì)不會(huì)存在了?
下面的實(shí)例清楚的告訴我們:之前的問(wèn)題就可以完美解決了。
import copy
l1 = [3, 4, a]
In [47]: l2 = copy.deepcopy(li)
In [48]: id(l1)
Out[48]: 1916632194312
In [49]: id(l2)
Out[49]: 1916634281416
In [50]: a[0] = 11
In [51]: id(l1)
Out[51]: 1916632194312
In [52]: id(l2)
Out[52]: 1916634281416
In [54]: l1
Out[54]: [3, 4, [11, 2]]
In [55]: l2
Out[55]: [1, 2, 3]
原理圖如下:
查漏補(bǔ)缺
為什么Python默認(rèn)的拷貝方式是淺拷貝?
時(shí)間角度:淺拷貝花費(fèi)時(shí)間更少
空間角度:淺拷貝花費(fèi)內(nèi)存更少
效率角度:淺拷貝只拷貝頂層數(shù)據(jù),一般情況下比深拷貝效率高。
本文知識(shí)點(diǎn)總結(jié):
不可變對(duì)象在賦值時(shí)會(huì)開(kāi)辟新空間
可變對(duì)象在賦值時(shí),修改一個(gè)的值,另一個(gè)也會(huì)發(fā)生改變
深、淺拷貝對(duì)不可變對(duì)象拷貝時(shí),不開(kāi)辟新空間,相當(dāng)于賦值操作
淺拷貝在拷貝時(shí),只拷貝第一層中的引用,如果元素是可變對(duì)象,并且被修改,那么拷貝的對(duì)象也會(huì)發(fā)生變化
深拷貝在拷貝時(shí),會(huì)逐層進(jìn)行拷貝,直到所有的引用都是不可變對(duì)象為止。
Python 中有多種方式實(shí)現(xiàn)淺拷貝,copy模塊的copy 函數(shù) ,對(duì)象的 copy 函數(shù) ,工廠方法,切片等。
大多數(shù)情況下,編寫(xiě)程序時(shí),都是使用淺拷貝,除非有特定的需求
淺拷貝的優(yōu)點(diǎn):拷貝速度快,占用空間少,拷貝效率高
聯(lián)系客服