Python深拷貝與淺拷貝的實(shí)現(xiàn)及對比詳解(很多人都理解錯(cuò)了)
前言
在 Python 中,進(jìn)行變量賦值、函數(shù)傳參、數(shù)據(jù)結(jié)構(gòu)復(fù)制等操作時(shí),經(jīng)常會(huì)遇到一個(gè)經(jīng)典問題:淺拷貝(Shallow Copy)與深拷貝(Deep Copy)的區(qū)別。很多初學(xué)者甚至一些有經(jīng)驗(yàn)的開發(fā)者,在面對嵌套結(jié)構(gòu)時(shí),常常因?yàn)槔斫獠簧疃瓤樱瑢?dǎo)致程序出現(xiàn)意想不到的 Bug。
一、從一個(gè)簡單的例子開始
先來看一段代碼,猜猜輸出結(jié)果是什么:
python original = [1, 2, [3, 4]] copy1 = original.copy() ? ? ? ? ?# 淺拷貝 copy2 = original[:] ? ? ? ? ? ? ?# 也是淺拷貝 original[2][0] = 99 original[0] = 100 print(original) ? # [100, 2, [99, 4]] print(copy1) ? ? ?# ??? print(copy2) ? ? ?# ???
如果你以為 copy1 和 copy2 都保持原樣 [1, 2, [3, 4]],就犯了經(jīng)典錯(cuò)誤。實(shí)際應(yīng)該是:
python print(copy1) ? # [1, 2, [99, 4]] print(copy2) ? # [1, 2, [99, 4]]
為什么 copy1 的第一個(gè)元素沒變,但內(nèi)部列表卻變了?這就要從淺拷貝的本質(zhì)說起。
二、Python 中的對象與引用
在理解拷貝之前,必須先掌握 Python 中變量與對象的關(guān)系。Python 中,變量名只是對象的一個(gè)標(biāo)簽或引用,實(shí)際的數(shù)據(jù)存儲(chǔ)在內(nèi)存中的對象里。
python a = [1, 2, 3] b = a ? ? ? ? ?# b 和 a 引用同一個(gè)對象 b.append(4) print(a) ? ? ? # [1, 2, 3, 4] ? a 也被修改了
這里 b = a 只是讓 b 指向了 a 所指向的對象,并沒有創(chuàng)建新的對象。這種賦值稱為引用賦值。
拷貝則是創(chuàng)建新對象,但根據(jù)復(fù)制深度不同,新對象與原對象的關(guān)系也不同。
三、淺拷貝(Shallow Copy)
1. 什么是淺拷貝
淺拷貝會(huì)創(chuàng)建一個(gè)新的對象,但它只復(fù)制原對象中元素的引用,而不會(huì)遞歸復(fù)制元素本身。換句話說,新對象中的子對象仍然是原對象中子對象的引用。
2. 淺拷貝的實(shí)現(xiàn)方式
Python 中常見的淺拷貝方式:
使用 copy 模塊的 copy() 函數(shù):import copy; new_obj = copy.copy(obj)
- 對于列表:list.copy() 或 list[:] 或 list(list_obj)
- 對于字典:dict.copy() 或 dict(dict_obj)
- 對于集合:set.copy()
python import copy lst = [1, 2, [3, 4]] lst_copy = copy.copy(lst) ? # 淺拷貝 lst_slice = lst[:] ? ? ? ? ? # 也是淺拷貝 lst_construct = list(lst) ? ?# 淺拷貝 print(lst_copy is lst) ? ? ? # False,外層對象不同 print(lst_copy[2] is lst[2]) # True,內(nèi)部列表是同一個(gè)
3. 淺拷貝的行為演示
python original = [1, 2, [3, 4]] shallow = original.copy() original[0] = 100 ? ? ? ?# 修改外層不可變元素(int) original[2][0] = 99 ? ? ?# 修改內(nèi)部可變列表 print(original) ? # [100, 2, [99, 4]] print(shallow) ? ?# [1, 2, [99, 4]]
修改 original[0] 時(shí),由于整數(shù)是不可變對象,shallow[0] 仍然指向原來的整數(shù) 1,所以不受影響。
修改 original[2][0] 時(shí),因?yàn)?original[2] 和 shallow[2] 指向的是同一個(gè)列表對象,所以修改會(huì)反映到 shallow 中。
關(guān)鍵點(diǎn):淺拷貝只保證外層容器是新對象,但內(nèi)部的元素依然是原對象的引用。
四、深拷貝(Deep Copy)
1. 什么是深拷貝
深拷貝會(huì)遞歸地復(fù)制原對象中的所有元素,創(chuàng)建一個(gè)完全獨(dú)立的新對象。新對象與原對象沒有任何共享的引用關(guān)系,修改原對象不會(huì)影響新對象,反之亦然。
2. 深拷貝的實(shí)現(xiàn)方式
使用 copy 模塊的 deepcopy() 函數(shù):import copy; new_obj = copy.deepcopy(obj)
python import copy lst = [1, 2, [3, 4]] deep = copy.deepcopy(lst) print(deep is lst) ? ? ? ?# False print(deep[2] is lst[2]) ?# False,內(nèi)部列表也被復(fù)制了
3. 深拷貝的行為演示
python import copy original = [1, 2, [3, 4]] deep = copy.deepcopy(original) original[0] = 100 original[2][0] = 99 print(original) ? # [100, 2, [99, 4]] print(deep) ? ? ? # [1, 2, [3, 4]] ?完全不受影響
深拷貝實(shí)現(xiàn)了真正的“備份”,無論原對象結(jié)構(gòu)多復(fù)雜,新對象都擁有自己獨(dú)立的數(shù)據(jù)。
五、深入理解:自定義對象的拷貝
1. 自定義類與 __copy__、__deepcopy__
對于自定義對象,copy.copy() 和 copy.deepcopy() 會(huì)調(diào)用對象的 __copy__() 和 __deepcopy__() 方法(如果定義了)。如果沒有定義,則默認(rèn)行為是:copy.copy() 會(huì)創(chuàng)建一個(gè)新實(shí)例,并遞歸復(fù)制所有屬性(淺拷貝)。copy.deepcopy() 會(huì)遞歸復(fù)制所有屬性(深拷貝)。
但我們可以通過自定義這些方法來控制復(fù)制行為。
python import copy class MyClass: ? ? def __init__(self, data): ? ? ? ? self.data = data ? ? def __copy__(self): ? ? ? ? # 自定義淺拷貝行為 ? ? ? ? new_instance = type(self)(self.data[:]) ? # 假設(shè) data 是列表 ? ? ? ? return new_instance ? ? def __deepcopy__(self, memo): ? ? ? ? # 自定義深拷貝行為 ? ? ? ? new_instance = type(self)(copy.deepcopy(self.data, memo)) ? ? ? ? return new_instance
2. 不可變對象的特殊情況
對于不可變對象(如 int, str, tuple 等),淺拷貝和深拷貝沒有區(qū)別,因?yàn)樗鼈儫o法被修改。但注意,如果元組中包含可變對象,深拷貝仍然會(huì)復(fù)制那些可變對象。
python import copy tup = (1, 2, [3, 4]) tup_copy = copy.copy(tup) tup_deep = copy.deepcopy(tup) print(tup_copy is tup) ? ? ? ?# True,元組本身不可變,copy 可能返回原對象 print(tup_deep is tup) ? ? ? ?# False,deepcopy 會(huì)遞歸復(fù)制內(nèi)部列表 print(tup_deep[2] is tup[2]) ?# False
六、淺拷貝與深拷貝的對比
| 特性 | 淺拷貝 | 深拷貝 |
| 復(fù)制層級(jí) | 只復(fù)制最外層對象 | 遞歸復(fù)制所有層級(jí) |
| 內(nèi)存占用 | 小,共享內(nèi)部對象 | 大,完全獨(dú)立 |
| 速度 | 快 | 慢 |
| 與原對象關(guān)系 | 內(nèi)部可變對象會(huì)相互影響 | 完全獨(dú)立,互不影響 |
| 適用場景 | 簡單數(shù)據(jù)結(jié)構(gòu),無需完全隔離 | 復(fù)雜嵌套結(jié)構(gòu),需要完全隔離 |
七、容易踩的坑
坑1:認(rèn)為 = 是拷貝
python a = [1, 2, 3] b = a b.append(4) print(a) ? # [1, 2, 3, 4] ? a 也被改了
很多人會(huì)以為 b = a 創(chuàng)建了一個(gè)副本,實(shí)際上只是多了一個(gè)引用。
坑2:混淆 copy() 與 deepcopy() 對嵌套結(jié)構(gòu)的影響
python original = [[1, 2], [3, 4]] shallow = original.copy() shallow[0][0] = 99 print(original) ?# [[99, 2], [3, 4]] ?原數(shù)據(jù)被改了!
期望的是復(fù)制,卻因?yàn)闇\拷貝共享了內(nèi)部列表,導(dǎo)致原數(shù)據(jù)被意外修改。
坑3:深拷貝時(shí)遇到循環(huán)引用
python import copy a = [] b = [a] a.append(b) ? # 形成循環(huán)引用 c = copy.deepcopy(a) ? # 默認(rèn)可以處理,不會(huì)無限遞歸 print(c) ?# [[[...]]]
deepcopy 內(nèi)部使用 memo 字典記錄已經(jīng)復(fù)制過的對象,可以安全處理循環(huán)引用。
坑4:認(rèn)為所有對象都能被深拷貝
有些對象(如文件對象、線程鎖、數(shù)據(jù)庫連接等)不能被復(fù)制,深拷貝會(huì)拋出異常。對于這類對象,通常需要自定義 __deepcopy__ 方法返回自身或合適的表示。
python
import copy
f = open('test.txt', 'w')
# copy.deepcopy(f) ? # TypeError: cannot pickle '_io.TextIOWrapper' object
八、實(shí)際應(yīng)用場景
場景1:防御性編程
當(dāng)需要將對象傳遞給不可信的函數(shù),且不希望函數(shù)修改原對象時(shí),可以用深拷貝。
python def process(data): ? ? # 可能會(huì)修改 data,但不想影響外部 ? ? data_local = copy.deepcopy(data) ? ? # ... 操作 data_local
場景2:緩存與狀態(tài)保存
在實(shí)現(xiàn)撤銷、重做功能時(shí),需要保存對象的歷史狀態(tài),深拷貝可以保證狀態(tài)獨(dú)立。
python class Document: ? ? def __init__(self): ? ? ? ? self.content = [] ? ? def save_state(self): ? ? ? ? return copy.deepcopy(self.content) ? ? def restore_state(self, state): ? ? ? ? self.content = state
場景3:多線程編程
多個(gè)線程需要獨(dú)立處理同一份數(shù)據(jù)副本時(shí),可以使用深拷貝避免競態(tài)條件。
場景4:配置管理
當(dāng)多個(gè)模塊需要基于同一份默認(rèn)配置生成自己的獨(dú)立配置時(shí),深拷貝可以防止互相干擾。
九、性能考量與最佳實(shí)踐
1. 避免不必要的深拷貝
深拷貝可能非常耗時(shí),尤其是對于大型對象。盡量只在必要時(shí)使用。
python # 如果只需要復(fù)制一層,用淺拷貝 new_list = old_list[:] # 如果內(nèi)部元素是不可變對象,淺拷貝就夠了 nums = [1, 2, 3] new_nums = nums[:] ? # 安全,因?yàn)檎麛?shù)不可變
2. 自定義復(fù)制方法提高效率
對于自定義類,可以重寫 __copy__ 和 __deepcopy__ 來優(yōu)化復(fù)制行為,比如共享某些不必復(fù)制的部分。
3. 利用 copyreg 模塊注冊復(fù)制函數(shù)
對于無法直接復(fù)制的對象(如第三方庫中的類型),可以通過 copyreg 模塊注冊復(fù)制函數(shù)。
十、總結(jié)
1. 淺拷貝:創(chuàng)建新對象,但內(nèi)部元素是原對象的引用。適合簡單結(jié)構(gòu)或不需要完全隔離的場景。
2. 深拷貝:遞歸復(fù)制所有內(nèi)容,得到完全獨(dú)立的新對象。適合復(fù)雜嵌套結(jié)構(gòu)且需要完全隔離的場景。
3. 賦值(=)不是拷貝,只是引用綁定。
4. 對于不可變對象,淺拷貝與深拷貝差別不大,但若內(nèi)部包含可變對象,深拷貝仍然會(huì)復(fù)制。
5. 使用 copy 模塊時(shí)注意性能開銷,避免濫用深拷貝。
到此這篇關(guān)于Python深拷貝與淺拷貝實(shí)現(xiàn)及對比的文章就介紹到這了,更多相關(guān)Python深拷貝與淺拷貝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解python opencv圖像混合算術(shù)運(yùn)算
這篇文章主要介紹了python opencv圖像混合算術(shù)運(yùn)算的相關(guān)知識(shí),本文通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-09-09
Django實(shí)現(xiàn)drf搜索過濾和排序過濾
當(dāng)我們需要對后臺(tái)的數(shù)據(jù)進(jìn)行過濾的時(shí)候,drf有兩種,搜索過濾和排序過濾。本文就詳細(xì)的介紹這兩種的實(shí)現(xiàn),感興趣的可以了解一下2021-06-06
詳解用Python調(diào)用百度地圖正/逆地理編碼API
這篇文章主要介紹了詳解用Python調(diào)用百度地圖正/逆地理編碼API,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
python3發(fā)送request請求及查看返回結(jié)果實(shí)例
這篇文章主要介紹了python3發(fā)送request請求及查看返回結(jié)果實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04
利用python為PostgreSQL的表自動(dòng)添加分區(qū)
這篇文章主要介紹了利用python為PostgreSQL的表自動(dòng)添加分區(qū),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01

