學習Python一年,這次終于弄懂了淺拷貝和深拷貝
話說,網(wǎng)上已經(jīng)有很多關(guān)于 Python 淺拷貝和深拷貝的文章了,不過好多文章看起來還是決定似懂非懂,所以決定用自己的理解來寫出這樣一篇文章。
當別人一提起Python中的復(fù)制操作,你會不會立馬站起來說:“我會”,于是就有了如下操作:
那淺拷貝和深拷貝有什么區(qū)別呢,你能給我講講嗎?
1、從引用vs.拷貝說起
首先,我們要弄清楚什么是對象引用與對象拷貝(復(fù)制)。
對象引用
Python中對象的賦值其實就是對象的引用。當創(chuàng)建一個對象,把它賦值給另一個變量的時候,Python并沒有拷貝這個對象,只是拷貝了這個對象的引用而已。
如果這個過程不理解,可以看看下圖:
當我們對 x 列表進行操作時,會發(fā)現(xiàn) y 中也發(fā)生了意料之外的事情:
由于列表是可變的,修改x這個列表對象的時候,也會改變對象 y 中對 x 的引用。
所以當我們在原處修改可變對象時 可能會影響程序中其他地方對相同對象的其他引用,這一點很重要。如果你不想這樣做,就需要明確地告訴 Python 復(fù)制該對象。
對象拷貝
如果你需要拷貝,可以進行如下操作:
- 沒有限制條件的分片表達式(L[:])
- 工廠函數(shù)(如list/dir/set)
- 字典copy方法(X.copy())
- copy標準庫模塊(import copy)
舉個例子,假設(shè)有一個列表L和一個字典D:
這樣定義之后,當你修改A和B時,會發(fā)現(xiàn)并不會對原來的L跟D產(chǎn)生影響,因為,這就是對象的拷貝。
上述對列表和字典的拷貝操作默認都為淺拷貝:
- 制作字典的淺層復(fù)制可以使用dict.copy() 方法
- 而制作列表的淺層復(fù)制可以通過賦值整個列表的切片完成,例如,copied_list = original_list[:]。
說到這里,疑問就產(chǎn)生了?什么是淺拷貝?淺拷貝的對應(yīng)深拷貝又該作何解釋?
2、談?wù)劀\拷貝和深拷貝
官方文檔定義:
淺層復(fù)制和深層復(fù)制之間的區(qū)別僅與復(fù)合對象 (即包含其他對象的對象,如列表或類的實例) 相關(guān):
一個淺層復(fù)制 會構(gòu)造一個新的復(fù)合對象,然后(在可能的范圍內(nèi))將原對象中找到的 引用 插入其中。
一個深層復(fù)制 會構(gòu)造一個新的復(fù)合對象,然后遞歸地將原始對象中所找到的對象的 副本 插入。
淺拷貝
?淺拷貝:拷貝了最外圍的對象本身,內(nèi)部的元素都只是拷貝了一個引用而已。也就是,把對象復(fù)制一遍,但是該對象中引用的其他對象我不復(fù)制。
用通俗的話理解就是:你的櫥柜(對象)里裝著一??(籃子)??(雞蛋),然后淺拷貝一下的意思。我只拷貝了最外面的這個櫥柜,至于里面的內(nèi)部元素(??和??)我并不拷貝。
當我們遇到簡單的對象時,用上面的解釋好像很好理解;如果遇到復(fù)合對象,就比如下列代碼:
代碼解釋:
- l2是l1的淺拷貝
- 把100追加到l1,對l2沒有影響
- 1內(nèi)部列表l1[1中的55刪除,對l2也產(chǎn)生影響,因為l1[1]和l2[1]綁定的是同一個列表
- 對可變對象來說,l2[1引用的列表進行+=就地修改列表。這次修改導致l1[1]也發(fā)生了改變
- 對元組來說,+= 運算符創(chuàng)建一個新元組,然后重新綁定給變量 l2[2]。這等同于l2[2] = l2[2] + (10, 11)?,F(xiàn)在,l1 和 l2 中最 后位置上的元組不是同一個對象
把這段代碼可視化出來如下:
動手試一試,可以點此處
深拷貝
?深拷貝:外圍和內(nèi)部元素都進行了拷貝對象本身,而不是引用。也就是,把對象復(fù)制一遍,并且該對象中引用的其他對象我也復(fù)制。
對比上面的籃子和雞蛋:你的櫥柜(對象)里裝著一??(籃子)??(雞蛋),然后深拷貝一下的意思。把最外面的這個櫥柜和里面的內(nèi)部元素(??和??)全部拷貝過來。
輸出結(jié)果:
拷貝的特點
- 不可變類型的對象(如數(shù)字、字符串、和其他'原子'類型的對象)對于深淺拷貝毫無影響,最終的地址值和值都是相等的。也就是,"obj is copy.copy(obj)" 、"obj is copy.deepcopy(obj)"
- 可變類型的對象=淺拷貝:值相等,地址相等copy淺拷貝:值相等,地址不相等deepcopy深拷貝:值相等,地址不相等
- 循環(huán)引用的對象如果對象有循環(huán)引用,那么這個樸素的算法會進入無限循環(huán)。deepcopy 函數(shù)會記住已經(jīng)復(fù)制的對象,因此能優(yōu)雅地處理循環(huán)引用。
循環(huán)引用:b 引用 a,然后追加到 a 中;deepcopy 會想辦法復(fù)制 a,而copy會進入無限循環(huán)。如下面代碼:
輸出結(jié)果:
深淺拷貝的作用
1,減少內(nèi)存的使用2,以后在做數(shù)據(jù)的清洗、修改或者入庫的時候,對原數(shù)據(jù)進行復(fù)制一份,以防數(shù)據(jù)修改之后,找不到原數(shù)據(jù)。3. 可以定制復(fù)制行為,通過實現(xiàn)__copy()和__deep__()方法來控制。
3、總結(jié)
看完這篇文章后,轉(zhuǎn)身就跟你同桌說:“x同學,聽說你最近在學Python,你知道淺拷貝和深拷貝嗎?”“不知道,學得有點暈”“沒事,我來給你講講:”
拷貝其實在開始學好幾個操作語句中,我們就已經(jīng)使用過卻可能不知道的(前3個),而且淺拷貝是Python的默認拷貝方式。拷貝的方法如下:
- 可變類型的切片操作:[:]
- 工廠函數(shù)(如list/dir/set)
- 字典copy方法(X.copy())
- 然后就是Python有專門的copy標準庫模塊:包含兩個方法copy()和deepcopy()
淺拷貝就像是我只拷貝最外圍的對象,對象中引用的其他對象我不復(fù)制。深拷貝就是完整的把對象和對象里的內(nèi)容都拷貝過來。拷貝的目的:
- 為了節(jié)省內(nèi)存
- 防止數(shù)據(jù)丟失。
后記:深淺拷貝的坑及難以理解的點也只在復(fù)合對象上,簡單對象就是我們平常理解的復(fù)制。而針對非容器類型(如數(shù)字、字符串、和其他'原子'類型的對象)沒有被拷貝一說。
要是你的同桌還是不懂,你就把這篇文章甩給他,讓他好好看看(偷笑)。如果你覺得這篇文章還不錯,請點個贊或者收個藏,點個關(guān)注更好啦。