在平時工作中,經(jīng)常涉及到數(shù)據(jù)的傳遞。在數(shù)據(jù)傳遞使用過程中,可能會發(fā)生數(shù)據(jù)被修改的問題。為了防止數(shù)據(jù)被修改,就需要再傳遞一個副本,即使副本被修改,也不會影響原數(shù)據(jù)的使用。為了生成這個副本,就產(chǎn)生了拷貝——今天就說一下Python中的深拷貝與淺拷貝的問題。
概念解讀
數(shù)據(jù)拷貝會涉及到Python中對象、可變類型、引用這3個概念,先來看看這幾個概念,只有明白了它們才能更好地理解拷貝到底是怎么一回事。
Python對象
在Python中,對對象有一種很通俗的說法,萬物皆對象。說的就是構(gòu)造的任何數(shù)據(jù)類型都是一個對象,無論是數(shù)字、字符串、還是函數(shù),甚至是模塊、Python都對當(dāng)做對象處理。
所有Python對象都擁有三個屬性:身份、類型、值。
看一個簡單的例子:
In [1]: name ="laowang"# name對象In [2]: id(name) # id:身份的唯一標(biāo)識Out[2]: 1698668550104In [3]:type(name)# type:對象的類型,決定了該對象可以保存什么類型的值Out[3]: strIn [4]: name # 對象的值,表示的數(shù)據(jù)Out[4]:'laowang'
可變與不可變對象
在Python中,按更新對象的方式,可以將對象分為2大類:可變對象與不可變對象。
可變對象: 列表、字典、集合。所謂可變是指可變對象的值可變,身份是不變的。
不可變對象:數(shù)字、字符串、元組。不可變對象就是對象的身份和值都不可變。新創(chuàng)建的對象被關(guān)聯(lián)到原來的變量名,舊對象被丟棄,垃圾回收器會在適當(dāng)?shù)臅r機(jī)回收這些對象。
In [7]: var1 ="python"In [8]:id(var1)Out[8]:1700782038408#由于var1是不可變的,重新創(chuàng)建了java對象,隨之id改變,舊對象python會在某個時刻被回收In [9]: var1 ="java"In [10]:id(var1)Out[10]:1700767578296
引用
在Python程序中,每個對象都會在內(nèi)存中申請開辟一塊空間來保存該對象,該對象在內(nèi)存中所在位置的地址被稱為引用。在開發(fā)程序時,所定義的變量名實(shí)際就對象的地址引用。
引用實(shí)際就是內(nèi)存中的一個數(shù)字地址編號,在使用對象時,只要知道這個對象的地址,就可以操作這個對象,但是因?yàn)檫@個數(shù)字地址不方便在開發(fā)時使用和記憶,所以使用變量名的形式來代替對象的數(shù)字地址。在Python中,變量就是地址的一種表示形式,并不開辟開辟存儲空間。
就像 IP 地址,在訪問網(wǎng)站時,實(shí)際都是通過 IP 地址來確定主機(jī),而 IP 地址不方便記憶,所以使用域名來代替 IP 地址,在使用域名訪問網(wǎng)站時,域名被解析成 IP 地址來使用。
通過一個例子來說明變量和變量指向的引用就是一個東西:
In [11]: age =18In [12]:id(age)Out[12]:1730306752In [13]:id(18)Out[13]:1730306752
逐步深入:引用賦值
上邊已經(jīng)明白,引用就是對象在內(nèi)存中的數(shù)字地址編號,變量就是方便對引用的表示而出現(xiàn)的,變量指向的就是此引用。賦值的本質(zhì)就是讓多個變量同時引用同一個對象的地址。
那么在對數(shù)據(jù)修改時會發(fā)生什么問題呢?
不可變對象的引用賦值
對不可變對象賦值,實(shí)際就是在內(nèi)存中開辟一片空間指向新的對象,原不可變對象不會被修改。原理圖如下:
下面通過案例來理解一下:
a與b在內(nèi)存中都是指向1的引用,所以a、b的引用是相同的。
In [1]: a =1In [2]: b = aIn [3]:id(a)Out[3]:1730306496In [4]:id(b)Out[4]:1730306496
現(xiàn)在再給a重新賦值,看看會發(fā)生什么變化?從下面不難看出:當(dāng)給a賦新的對象時,將指向現(xiàn)在的引用,不在指向舊的對象引用。
In [1]: a =1In [2]: b = aIn [5]: a =2In [6]:id(a)Out[6]:1730306816In [7]:id(b)Out[7]:1730306496
可變對象的引用賦值
可變對象保存的并不是真正的對象數(shù)據(jù),而是對象的引用。當(dāng)對可變對象進(jìn)行賦值時,只是將可變對象中保存的引用指向了新的對象。原理圖如下:
仍然通過一個實(shí)例來體會一下,可變對象引用賦值的過程:當(dāng)改變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
主旨詳解:淺拷貝、深拷貝
經(jīng)過前2部分的解讀,大家對對象的引用賦值應(yīng)該有了一個清晰的認(rèn)識了。那么Python中如何解決原始數(shù)據(jù)在函數(shù)傳遞之后不受影響?這個問題Python已經(jīng)幫我們解決了,使用對象的拷貝或者深拷貝就可以愉快解決了。
下面具體來看看Python中的淺拷貝與深拷貝是如何實(shí)現(xiàn)的。
淺拷貝
為了解決函數(shù)傳遞后被修改的問題,就需要拷貝一份副本,將副本傳遞給函數(shù)使用,就算是副本被修改,也不會影響原始數(shù)據(jù) 。
不可變對象的拷貝
不可變對象只在修改的時候才會在內(nèi)存中開辟新的空間,而拷貝實(shí)際上是讓多個對象同時指向一個引用,和對象的賦值沒區(qū)別。
同樣的,通過一個實(shí)例來感受一下:不難看出,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
可變對象的拷貝
對于不可變對象的拷貝,對象的引用并沒有發(fā)生變化,那么可變對象的拷貝會不會和不可變對象一樣了?我們接著往下看。
通過下面的實(shí)例能看出:可變對象的拷貝會在內(nèi)存中開辟一個新的空間來保存拷貝的數(shù)據(jù)。當(dāng)再改變之前的對象時,對拷貝之后的對象沒有任何影響。
In [24]: importcopyIn [25]: l1 = [1,2,3]In [26]: l2 =copy.copy(l1)In [27]:id(l1)Out[27]:1916631742088In [28]:id(l2)Out[28]:1916636282952In [29]: l1[0] =11In [30]:id(l1)Out[30]:1916631742088In [31]:id(l2)Out[31]:1916636282952
原理圖如下:
現(xiàn)在再回到剛才那個問題,是不是淺拷貝就可以解決原始數(shù)據(jù)在函數(shù)傳遞之后不變的問題了?下面看一個稍微復(fù)雜一點(diǎn)的數(shù)據(jù)結(jié)構(gòu)。
通過下面這個實(shí)例可以發(fā)現(xiàn):復(fù)雜對象在拷貝時,并沒有解決數(shù)據(jù)在傳遞之后,數(shù)據(jù)改變的問題。出現(xiàn)這種原因,是copy() 函數(shù)在拷貝對象時只是將指定對象中的所有引用拷貝了一份,如果這些引用當(dāng)中包含了一個可變對象的話,那么數(shù)據(jù)還是會被改變。這種拷貝方式,稱為淺拷貝。
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還提供了另一種拷貝方式(深拷貝)來解決。
深拷貝
區(qū)別于淺拷貝只拷貝頂層引用,深拷貝會逐層進(jìn)行拷貝,直到拷貝的所有引用都是不可變引用為止。
接下來我們看看,要是將上邊的拷貝實(shí)例用使用深拷貝的話,原始數(shù)據(jù)改變的問題還會不會存在了?
下面的實(shí)例清楚地告訴我們:之前的問題就可以完美解決了。
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, 4]
原理圖如下:
查漏補(bǔ)缺
為什么Python默認(rèn)的拷貝方式是淺拷貝?
時間角度:淺拷貝花費(fèi)時間更少;
空間角度:淺拷貝花費(fèi)內(nèi)存更少;
效率角度:淺拷貝只拷貝頂層數(shù)據(jù),一般情況下比深拷貝效率高。
本文知識點(diǎn)總結(jié):
不可變對象在賦值時會開辟新空間;
可變對象在賦值時,修改一個的值,另一個也會發(fā)生改變;
深、淺拷貝對不可變對象拷貝時,不開辟新空間,相當(dāng)于賦值操作;
淺拷貝在拷貝時,只拷貝第一層中的引用,如果元素是可變對象,并且被修改,那么拷貝的對象也會發(fā)生變化;
深拷貝在拷貝時會逐層進(jìn)行拷貝,直到所有的引用都是不可變對象為止;
Python中有多種方式實(shí)現(xiàn)淺拷貝,copy模塊的copy函數(shù)、對象的copy函數(shù)、工廠方法、切片等;
大多數(shù)情況下,編寫程序時都是使用淺拷貝,除非有特定的需求;
淺拷貝的優(yōu)點(diǎn):拷貝速度快,占用空間少,拷貝效率高。
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3052瀏覽量
74286 -
python
+關(guān)注
關(guān)注
56文章
4807瀏覽量
85016
原文標(biāo)題:Python 程序員如何防止數(shù)據(jù)被修改?
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論