Python中的深淺拷貝
在講深淺拷貝之前,我們先重溫一下 is
和==
的區別。
在判斷物件是否相等比較的時候我們可以用is
和 ==
- is:比較兩個物件的引用是否相同,即 它們的id 是否一樣
- == : 比較兩個物件的值是否相同。
id() ,是Python的一個內建函式,返回物件的唯一標識,用於獲取物件的記憶體地址。
如下
首先,會為整數1分配一個記憶體空間。 變數a 和 b 都指向了這個記憶體空間(記憶體地址相等),所以他們的id相等。
即 a is b
為 True
但是,真的所有整數數字都這樣嗎? 答案是:不是! 只有在 -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就是一個巢狀的子物件,指向了另外一個記憶體空間。所以淺拷貝只是拷貝了元素1
, 2
和子物件的引用!
另外一種情況,如果巢狀的是一個元組呢?
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資料彙總(長期更新)。隔壁小孩都饞哭了 --- 點選領取