天啦嚕!僅僅5張圖,徹底搞懂Python中的深淺拷貝

阿亮亮亮阿發表於2020-11-13

Python中的深淺拷貝

在講深淺拷貝之前,我們先重溫一下 is==的區別。

在判斷物件是否相等比較的時候我們可以用is==

  • is:比較兩個物件的引用是否相同,即 它們的id 是否一樣
  • == : 比較兩個物件的值是否相同。
id() ,是Python的一個內建函式,返回物件的唯一標識,用於獲取物件的記憶體地址。

如下

首先,會為整數1分配一個記憶體空間。 變數a 和 b 都指向了這個記憶體空間(記憶體地址相等),所以他們的id相等。

a is bTrue

但是,真的所有整數數字都這樣嗎? 答案是:不是! 只有在 -25 ~ 256範圍中的整數才不會重新分配記憶體空間。

如下所示:

因為257 超出了範圍,所以id不相同,所以a is b返回的值為False。

>>> a = 257
>>> b = 257
>>> print(id(a))
20004752
>>> print(id(b))
20001312
>>> print(a is b)
False
>>> print(a == b)
True

這樣做是考慮到效能,Python對-5 到 256 的整數維護了一個陣列,相當於一個快取, 當數值在這個範圍內,直接就從陣列中返回相對應的引用地址了。如果不在這個範圍內,會重新開闢一個新的記憶體空間。

is 和 == 哪個效率高?

相比之下,is比較的效率更高,因為它只需要判斷兩個物件的id是否相同即可。

== 則需要過載__eq__ 這個函式,遍歷變數中的所有元素內容,逐次比較是否相同。因此效率較低

淺拷貝 深拷貝

給變數進行賦值,有兩種方法 直接賦值,拷貝

直接賦值就 = 就可以了。而拷貝又分為淺拷貝和深拷貝

先說結論吧:

  • 淺拷貝:拷貝的是物件的引用,如果原物件改變,相應的拷貝物件也會發生改變
  • 深拷貝:拷貝物件中的每個元素,拷貝物件和原有物件不在有關係,兩個是獨立的物件

光看上面的概念,對新手來講可能不太好理解。來看下面的例子吧

賦值

a = [1, 2, 3]
b = a
print(id(a)) # 52531048
print(id(b)) # 52531048

定義變數a,同時將a賦值給b。列印之後發現他們的id是相同的。說明指向了同一個記憶體地址。

然後修改a的值,再檢視他們的id

a = [1, 2, 3]
b = a
print(id(a))  # 46169960
a[1] = 0
print(a, b)  # [1, 0, 3] [1, 0, 3]
print(id(a))  # 46169960
print(id(b))  # 46169960

這時候發現修改後的a和b以及最開始的a的記憶體地址是一樣的。也就是說a和b還是指向了那一塊記憶體,只不過記憶體裡面的[1, 2, 3] 變成了[1, 0, 3]

因為每次重新執行的時候記憶體地址都是發生改變的,此時的id(a) 的值46169960與52531048是一樣的

所以我們就可以判斷出,b和a的引用是相同的,當a發生改變的時候,b也會發生改變。

賦值就是:你a無論怎麼變,你指向誰,我b就跟著你指向誰。

拷貝

提到拷貝就避免不了可變物件和不可變物件。

  • 可變物件:當有需要改變物件內部的值的時候,這個物件的id不發生變化。
  • 不可變物件:當有需要改變物件內部的值的時候,這個物件的id會發生變化。
a = [1, 2, 3]
print(id(a)) # 56082504
a.append(4)
# 修改列表a之後 id沒發生改變,可變物件
print(id(a)) # 56082504

a = 'hello'
print(id(a)) # 59817760
a = a + ' world'
print(id(a)) # 57880072
# 修改字串a之後,id發生了變化。不可變物件
print(a) # hello world

淺拷貝

拷貝的是不可變物件,一定程度上來講等同於賦值操作。但是對於多層巢狀結構,淺拷貝只拷貝父物件,不拷貝內部的子物件。

使用copy模組的 copy.copy 進行淺拷貝。

import copy
a = [1, 2, 3]
b = copy.copy(a)
print(id(a))  # 55755880
print(id(b))  # 55737992
a[1] = 0
print(a, b) # [1, 0, 3] [1, 2, 3]

通俗的講,我將現在的a 複製一份重新分配了一個記憶體空間。後面你a怎麼改變,那跟我b是沒有任何關係的。

對於列表的淺拷貝還可以通過list(), list[:] 來實現

但是!我前面提到了對於多層巢狀的結構,需要注意

看下面的例子

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)

print(id(a)) # 23967528
print(id(b)) # 21738984
# 改變a中的子列表
a[-1].append(5)
print(a) # [1, 2, [3, 4, 5]]
print(b) # [1, 2, [3, 4, 5]]  ?? 為什麼不是[1, 2, [3, 4]]呢?

b是由a淺拷貝得到的。我修改了a中巢狀的列表,發現b也跟著修改了?

如果還是不太理解,可以參考下圖。LIST就是一個巢狀的子物件,指向了另外一個記憶體空間。所以淺拷貝只是拷貝了元素12 和子物件的引用!

另外一種情況,如果巢狀的是一個元組呢?

import copy
a = [1, 2, (3, 4)]
b = copy.copy(a)

# 改變a中的元組
a[-1] += (5,)
print(a) # [1, 2, (3, 4, 5)]
print(b) # [1, 2, (3, 4)]

我們發現淺拷貝得來的b並沒有發生改變。因為元組是不可變物件。改變了元組就會生成新的物件。b中的元組引用還是指向了舊的元組。

深拷貝

所謂深拷貝呢,就是重新分配一個記憶體空間(新物件),將原物件中的所有元素通過遞迴的方式進行拷貝到新物件中。

在Python中 通過copy.deepcopy() 來實現深拷貝。

import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)

print(id(a)) # 66587176
print(id(b)) # 66587688
# 改變a中的可變物件
a[-1].append(5)
print(a) # [1, 2, [3, 4, 5]]
print(b) # [1, 2, [3, 4]]  深拷貝之後字列表不會受原來的影響

結語

1、深淺拷貝都會對源物件進行復制,佔用不同的記憶體空間

2、如果源物件沒有子目錄,則淺拷貝只能拷貝父目錄,改動子目錄時會影響淺拷貝的物件

3、列表的切片本質就是淺拷貝


史上最全Python資料彙總(長期更新)。隔壁小孩都饞哭了 --- 點選領取

相關文章