Python的深淺拷貝講解!

來源:Datawhale

本文約3100字,建議閱讀6分鐘

本文詳細介紹了Python中的深淺拷貝的相關知識。


前言

在很多語言中都存在深淺拷貝兩種拷貝數據的方式,Python中也不例外。本文中詳細介紹了Python中的深淺拷貝的相關知識,文章的內容包含:

  • 對象、數據類型、引用

  • 賦值

  • 淺拷貝

  • 深拷貝

一、Python對象

我們經常聽到:在Python中一切皆對象。其實,說的就是我們在Python中構造的任何數據類型都是一個對象,不管是數字、字符串、字典等常見的數據結構,還是函數,甚至是我們導入的模塊等,Python都會把它當做是一個對象來處理。

所有的Python對象都擁有3個屬性:

  • 身份

  • 類型

我們看一個簡單的例子來理解上面的3個屬性:

假設我們聲明瞭一個name變量,通過id、type方法能夠查看對象的身份和類型:

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

Python中,萬物皆對象


二、數據類型

2.1 可變和不可變類型

在Python中,按照更新對象的方式,我們可以將對象分爲2大類:可變數據類型和不可變數據類型。

  • 不可變數據類型:數值、字符串、布爾值。不可變對象就是對象的身份和值都不可變。新創建的對象被關聯到原來的變量名,舊對象被丟棄,垃圾回收器會在適當的時機回收這些對象。

  • 可變數據類型:列表、字典、集合。所謂的可變指的是可變對象的值可變,但是身份是不可變的。

首先我們看看不可變對象:

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

也就是說,這個變量在計算機內存中的位置是不變的,只是換了一個名字來存放,來看3個實際的例子:

以上的例子說明:當我們對字符串、數值型、布爾值的數據改變變量名,並不會影響到數據在內存中的位置。

我們看看可變類型的例子,列表、字典、集合都是一樣的效果:

雖然是相同的數據,但是變量名字不同,內存中仍然會開闢新的內存地址來進行存放相同的數據,我們以字典爲例:

2.2 引用

在Python語言中,每個對象都會在內存中申請開闢一塊新的空間來保存對象;對象在內存中所在位置的地址稱之爲引用。

可以說,我們定義的變量名實際上就是對象的地址引用。引用實際上就是內存中的一個數字地址編號。在使用對象的時候,只要知道這個對象的地址,我們就可以操作這個對象。

因爲這個數字地址不太容易記憶,所以我們使用變量名的形式來代替對象的數字地址。在Python中,變量就是地址的一種表示形式,並不會開闢新的存儲空間。

我們通過一個例子來說明變量和變量指向的引用(內存地址)實際上就是一個東西:

三、賦值

3.1 相同數據,不同變量名

討論完Python的對象、屬性和引用3個重要的概念之後,在正式介紹深淺拷貝之前,我們先討論Python中的賦值。

在Python中,每次賦值都會開闢新的內存地址來存放數據,比如我們同時存放一個列表[1,2,3],即使數據是相同的,但是內存地址卻不同:

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

我們給v1列表追加了一個元素,發現它的內存地址是不變的,當然v2肯定是不變的:


3.2 一個變量多次賦值

如果我們對一個變量多次賦值,其內存是會變化的:

3.3 變量賦值

將一個變量賦值給另一個變量,其實它們就是同一個對象:數據相同,在內存中的地址也相同:

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

實際上它們就是同一個對象!!!!

3.4 嵌套賦值

如果是列表中嵌套着另外的列表,那麼當改變其中一個列表的時候,另一個列表中的也會隨着改變:

原始數據信息:

當我們給v1追加了新元素之後:

總結:賦值其實就是將一個對象的地址賦值給一個變量,使得變量指向該內存地址。

四、淺拷貝

在Python中進行拷貝之前,我們需要導入模塊:

import copy


⚠️淺拷貝只是拷貝數據的第一層,不會拷貝子對象。

4.1 不可變類型的淺拷貝

如果只是針對不可變的數據類型(字符串、數值型、布爾值),淺拷貝的對象和原數據對象是相同的內存地址:

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

image-20201115225938833

4.2 可變類型的淺拷貝

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


從上面的例子看出來:

  • 列表本身的淺拷貝對象的地址和原對象的地址是不同的,因爲列表是可變數據類型。

  • 列表中的元素(第1個元素爲例)和淺拷貝對象中的第一個元素的地址是相同的,因爲元素本身是數值型,是不可變的。

通過一個圖形來說明這個關係:

字典中也存在相同的情況:字典本身的內存地址不同,但是裏面的鍵、值的內存地址是相同的,因爲鍵值都是不可變類型的數據。

如果可變類型的數據中存在嵌套的結構:

從上面的兩個例子中我們可以看出來:

在可變類型的數據中,如果存在嵌套的結構類型,淺拷貝只複製最外層的數據,導致內存地址發生變化,裏面數據的內存地址不會變。

五、深拷貝

深拷貝不同於淺拷貝的是:深拷貝會拷貝所有的可變數據類型,包含嵌套的數據中的可變數據。深拷貝是變量對應的值複製到新的內存地址中,而不是複製數據對應的內存地址。

5.1 不可變類型的深拷貝

關於不可變類型的深淺拷貝,其效果是相同的,具體看下面的例子:

我們得出一個結論:針對不可變數據類型的深淺拷貝,其結果是相同的。

5.2 可變類型的深拷貝

首先我們討論的是不存在嵌套的情況:

針對列表數據:

針對字典數據:

我們可以得出結論:

  • 深拷貝對最外層數據是隻拷貝數據,會開闢新的內存地址來存放數據。

  • 深拷貝對裏面的不可變數據類型直接複製數據和地址,和可變類型的淺拷貝是相同的效果。

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

結論1:對整個存在嵌套類型的數據進行深淺拷貝都會發生內存的變化,因爲數據本身是可變的。

結論2:我們查看第一個元素1的內存地址,發生三者是相同的,因爲1是屬於數值型,是不可變類型。

結論3:我們查看第三個元素即裏面嵌套列表的內存,發現只有深拷貝是不同的,因爲這個嵌套的列表是可變數據類型,深拷貝在拷貝了最外層之後還會繼續拷貝子層級的可變類型。

結論4:我們查看嵌套列表中的元素的內存地址,發現它們是相同的,因爲元素是數值型,是不可變的,不受拷貝的影響。

六、元組的深淺拷貝

元組本身是不可變數據類型,但是其中的值是可以改變的,內部可以有嵌套可變數據類型,比如列表等,會對它的拷貝結果造成影響。

6.1 不存在嵌套結構

當元組中不存在嵌套結構的時候,元組的深淺拷貝是相同的效果:

6.2 存在嵌套結構

當元組的數據中存在嵌套的可變類型,比如列表等,深拷貝會重新開闢地址,將元組重新成成一份。

七、is和==

在文章的開始就已經談過:在Python中每個變量都有自己的標識、類型和值。每個對象一旦創建,它的標識就絕對不會變。一個對象的標識,我們可以理解成其在內存中的地址。is()運算符比較的是兩個對象的標識;id()方法返回的就是對象標識的整數表示。

總結:is()比較對象的標識;==運算符比較兩個對象的值(對象中保存的數據)。在實際的編程中,我們更多關注的是值,而不是標識本身。

第一個例子:我們創建了兩個不同的對象,只是它們的值剛好相同而已。

第二個例子:我們先創建了一個對象v3,然後將他賦值給另一個對象v4,其實它們就是相同的對象,所以標識(內存地址)是相同的,只是它們的名字不同而已。

總結

通過大量的例子,我們得出結論:

  • 在不可變數據類型中,深淺拷貝都不會開闢新的內存空間,用的都是同一個內存地址。

  • 在存在嵌套可變類型的數據時,深淺拷貝都會開闢新的一塊內存空間;同時,不可變類型的值還是指向原來的值的地址。

不同的是:在嵌套可變類型中,淺拷貝只會拷貝最外層的數據,而深拷貝會拷貝所有層級的可變類型數據。

編輯:王菁

校對:王欣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章