歡迎光臨
每天分享高質量文章

Python 程式員如何防止資料被修改?

來源:CSDN

ID:CSDNnews

在平時工作中,經常涉及到資料的傳遞。在資料傳遞使用過程中,可能會發生資料被修改的問題。為了防止資料被修改,就需要再傳遞一個副本,即使副本被修改,也不會影響原資料的使用。為了生成這個副本,就產生了拷貝——今天就說一下Python中的深拷貝與淺拷貝的問題。

 

概念解讀

 

 

資料拷貝會涉及到Python中物件、可變型別、取用這3個概念,先來看看這幾個概念,只有明白了它們才能更好地理解拷貝到底是怎麼一回事。

 

Python物件

在Python中,對物件有一種很通俗的說法,萬物皆物件。說的就是構造的任何資料型別都是一個物件,無論是數字、字串、還是函式,甚至是模塊、Python都對當做物件處理。

 

所有Python物件都擁有三個屬性:身份、型別、值。

 

看一個簡單的例子:

 

In [1]: name = “laowang” # name物件

In [2]: id(name)  # id:身份的唯一標識
Out[2]: 1698668550104

In [3]: type(name) # type:物件的型別,決定了該物件可以儲存什麼型別的值
Out[3]: str

In [4]: name  # 物件的值,表示的資料
Out[4]: ‘laowang’


 

可變與不可變物件

在Python中,按更新物件的方式,可以將物件分為2大類:可變物件與不可變物件。

 

  • 可變物件:  串列、字典、集合。所謂可變是指可變物件的值可變,身份是不變的。

  • 不可變物件:數字、字串、元組。不可變物件就是物件的身份和值都不可變。新創建的物件被關聯到原來的變數名,舊物件被丟棄,垃圾回收器會在適當的時機回收這些物件。

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]: 1700767578296


取用

在Python程式中,每個物件都會在記憶體中申請開闢一塊空間來儲存該物件,該物件在記憶體中所在位置的地址被稱為取用。在開發程式時,所定義的變數名實際就物件的地址取用。

 

取用實際就是記憶體中的一個數字地址編號,在使用物件時,只要知道這個物件的地址,就可以操作這個物件,但是因為這個數字地址不方便在開發時使用和記憶,所以使用變數名的形式來代替物件的數字地址。在Python中,變數就是地址的一種表示形式,並不開闢開闢儲存空間。

 

就像 IP 地址,在訪問網站時,實際都是通過 IP 地址來確定主機,而 IP 地址不方便記憶,所以使用域名來代替 IP 地址,在使用域名訪問網站時,域名被解析成 IP 地址來使用。

 

通過一個例子來說明變數和變數指向的取用就是一個東西:

 

In [11]: age = 18

In [12]: id(age)
Out[12]: 1730306752

In [13]: id(18)
Out[13]: 1730306752


 

逐步深入:取用賦值

 

 

上邊已經明白,取用就是物件在記憶體中的數字地址編號,變數就是方便對取用的表示而出現的,變數指向的就是此取用。賦值的本質就是讓多個變數同時取用同一個物件的地址。

 

那麼在對資料修改時會發生什麼問題呢?

 

不可變物件的取用賦值

 

對不可變物件賦值,實際就是在記憶體中開闢一片空間指向新的物件,原不可變物件不會被修改。原理圖如下:

 

 

下麵通過案例來理解一下:

 

a與b在記憶體中都是指向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


現在再給a重新賦值,看看會發生什麼變化?從下麵不難看出:當給a賦新的物件時,將指向現在的取用,不在指向舊的物件取用。

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


 

可變物件的取用賦值

 

可變物件儲存的並不是真正的物件資料,而是物件的取用。當對可變物件進行賦值時,只是將可變物件中儲存的取用指向了新的物件。原理圖如下:

 

 

仍然通過一個實體來體會一下,可變物件取用賦值的過程:當改變l1時,整個串列的取用會指新的物件,但是l1與l2都是指向儲存的同一個串列的取用,所以取用地址不會變。

In [3]: l1 = [123]

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


 

主旨詳解:淺拷貝、深拷貝

 

 

經過前2部分的解讀,大家對物件的取用賦值應該有了一個清晰的認識了。那麼Python中如何解決原始資料在函式傳遞之後不受影響?這個問題Python已經幫我們解決了,使用物件的拷貝或者深拷貝就可以愉快解決了。

 

下麵具體來看看Python中的淺拷貝與深拷貝是如何實現的。

 

淺拷貝

 

為瞭解決函式傳遞後被修改的問題,就需要拷貝一份副本,將副本傳遞給函式使用,就算是副本被修改,也不會影響原始資料 。

 

不可變物件的拷貝

 

不可變物件只在修改的時候才會在記憶體中開闢新的空間,而拷貝實際上是讓多個物件同時指向一個取用,和物件的賦值沒區別。

 

同樣的,通過一個實體來感受一下:不難看出,a與b指向相同的取用,不可變物件的拷貝就是物件賦值。

 

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


可變物件的拷貝

 

對於不可變物件的拷貝,物件的取用並沒有發生變化,那麼可變物件的拷貝會不會和不可變物件一樣了?我們接著往下看。

 

通過下麵的實體能看出:可變物件的拷貝會在記憶體中開闢一個新的空間來儲存拷貝的資料。當再改變之前的物件時,對拷貝之後的物件沒有任何影響。

In [24]: import copy

In [25]: l1 = [123]

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


 

原理圖如下:

 

 

現在再回到剛纔那個問題,是不是淺拷貝就可以解決原始資料在函式傳遞之後不變的問題了?下麵看一個稍微複雜一點的資料結構。

 

通過下麵這個實體可以發現:複雜物件在拷貝時,並沒有解決資料在傳遞之後,資料改變的問題。出現這種原因,是copy() 函式在拷貝物件時只是將指定物件中的所有取用拷貝了一份,如果這些取用當中包含了一個可變物件的話,那麼資料還是會被改變。這種拷貝方式,稱為淺拷貝。

 

In [35]: a = [12]

In [36]: l1 = [34, 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]: [34, [112]]

In [44]: l2
Out[44]: [34, [112]]


原理圖如下:

 

 

對於上邊這種狀況,Python還提供了另一種拷貝方式(深拷貝)來解決。

 

深拷貝

 

區別於淺拷貝只拷貝頂層取用,深拷貝會逐層進行拷貝,直到拷貝的所有取用都是不可變取用為止。

 

接下來我們看看,要是將上邊的拷貝實體用使用深拷貝的話,原始資料改變的問題還會不會存在了?

 

下麵的實體清楚地告訴我們:之前的問題就可以完美解決了。

 

import copy

l1 = [34, 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]: [34, [112]]

In [55]: l2
Out[55]: [[1, 2], 3, 4]


 

原理圖如下:

 

 

 

查漏補缺

 

 

為什麼Python預設的拷貝方式是淺拷貝?

 

  • 時間角度:淺拷貝花費時間更少;

  • 空間角度:淺拷貝花費記憶體更少;

  • 效率角度:淺拷貝只拷貝頂層資料,一般情況下比深拷貝效率高。

 

本文知識點總結:

 

  • 不可變物件在賦值時會開闢新空間;

  • 可變物件在賦值時,修改一個的值,另一個也會發生改變;

  • 深、淺拷貝對不可變物件拷貝時,不開闢新空間,相當於賦值操作;

  • 淺拷貝在拷貝時,只拷貝第一層中的取用,如果元素是可變物件,並且被修改,那麼拷貝的物件也會發生變化;

  • 深拷貝在拷貝時會逐層進行拷貝,直到所有的取用都是不可變物件為止;

  • Python中有多種方式實現淺拷貝,copy模塊的copy函式、物件的copy函式、工廠方法、切片等;

  • 大多數情況下,編寫程式時都是使用淺拷貝,除非有特定的需求;

  • 淺拷貝的優點:拷貝速度快,占用空間少,拷貝效率高。

 

作者:PayneLi,就職於深圳一家科技公司AI部門,負責Python後臺開發、爬蟲和資料挖掘相關工作。運營個人公眾號 Python全家桶 ,歡迎關註,一起學習交流。

宣告:本文為作者投稿,版權歸其個人所有。

赞(0)

分享創造快樂