詳解 Python 淺拷貝與深拷貝以及引用

一. Python 變量及其存儲

1.1 Python 變量存儲情況

在高級語言中,變量是對內存及其地址的抽象。對於python 而言,python 的一切變量都是對象,變量的存儲,採用了引用語義的方式,存儲的只是一個變量的值所在的內存地址,而不是指這個變量的本身:

  • 引用語義:在 python 中,變量保存的是對象(值)的引用,我們稱爲 引用語義。採用這種方式,變量所需的存儲空間大小一致,因爲變量只是保存了一個引用。也被稱爲對象語義指針語義

  • 值語義:有些語言採用的不是這種方式,它們把變量的值直接保存在變量的存儲區裏,這種方式被我們稱爲值語義,例如 C 語言,採用這種存儲方式,每一個變量在內存中所佔的空間就要根據變量實際的大小而定,無法固定下來。

  • 值語義和引用語義的區別:

    • 值語義: 死的、 傻的、 簡單的、 具體的、 可複製的
  • 引用語義: 活的、 聰明的、 複雜的、 抽象的、 不可複製的

1.2 Python 數據類型地址存儲

Python 的引用語義和 C 語言值語義在內存中的存儲情況如下:

變量的每一次初始化,都會開闢了一個新的空間,將新內容的地址賦值給變量。對於下圖來說,如果重複的給 str1 賦值,其在內存中的變化如下右圖:


str1 在重複的初始化過程中,str1 中存儲的元素地址由 ‘Hello world’ 的地址變成了 ‘new Hello world’ 的。

瞭解了 Python 變量賦值過程後,就應該明白 Python 的賦值過程相當於完全共享資源,一個值的改變會完全被另一個值共享。而實際應用是往往需要備份出一份數據而只對新值進行修改,這時候賦值就變得不是那麼明智。引用 Python 淺拷貝與深拷貝概念爲這方面提供瞭解決方案。

二. 深淺拷貝引入

經過上面討論,我們知道在 Python 中對對象的賦值其實就是對對象的引用。當創建一個對象,把它賦值給另一個變量的時候,Python 並沒有拷這個對象,只是拷貝了這個引用對象而已。

考慮這樣一個場景:有一個列表 warmtones 包含幾種顏色,每個顏色代表元素類的一個實例。而希望創建一個爲名爲 palette 的列表,使 palette 具有跟 warmtones 一樣的顏色列表,並希望達到在改變 palette 的同時不改變 warmtones。

於是,A,B,C 同學發表了自己的看法。

A 同學:

palette = warmtones

B 同學:

palette = list(warmones)

C 同學:

import copy
palette = copy.deepcopy(warmtones) 

**注意:**爲了簡化對比說明,使用實例中的顏色不再是一個類的實例,而是使用字符串替代。
先考慮 A 同學,現在想給 warmtones 添加 black 顏色:

很不幸,這並不符合實際要求,因爲在對 palette 進行添加顏色的時候,warmtones 也發生了改變。實際上 A 同學的做法,可以用以下圖說明:

A 同學的做法相當於 將 warmtonespalette 指向了同一塊內存地址,所以無論是改變 warmtones 還是 palette 都會對此內存地址數據產生變化。而導致改變 warmtonespalette 任意一方,都會影響到對方。


對於 B 同學, B同學使用的是一種淺拷貝:

這裏對 palette 添加 _black 元素,此時看起來並不會對 warmtones產生影響,這就達到要求了嗎?其實,不然,分析圖中,很容易見,雖然 warmtonespaletteid 已經不同了, 但它們的元素 id 仍然相同。如下圖:

現在對 B 同學的做法使用另一個實例,將原來 warmtones 列表字符串元素換成 Color 類的實例對象

如你所見,直接將 palette 列表中的對象改變,不會影響,warmtones ,因爲 yellow 是 Color 的一個新實例,palette 的 id[0] 已經被指向新的地址,故不會影響 warmtones。見下圖:

但如果直接對 paltte 地址中的數據進行修改,如將 red 實例對象顏色修改爲 yellow, 則此時 warmtones 也發生改變。

也就是說只要改變這些地址相同的元素值,warmtonespalette 還是會互相影響,如圖:


對於 C 同學的做法中,palettewarmtones 的深拷貝,palette 的引用對象列表也是從 warmtones 中複製過來的。

結合實例:

此時,可以看到 palette 中所有元素地址已經被指向了另一個地方,warmtones對象全部被引用到了 palette

上面所討論的問題涉及到 python 引用機制和淺複製及深複製

三. Python 淺拷貝與深拷貝

3.1 什麼是淺拷貝、深拷貝?

  • 淺拷貝:只是拷貝了對象的最外圍本身(即只拷貝一層),而其內部元素也只是拷貝了一個引用而已(即其他內部對象還只是引用),對象中的其他對象並不複製。

  • 深拷貝:外圍以及內部元素都進行拷貝對象本身,而不是引用,其他對象的對象一併複製(即遞歸賦值所有對象)。

3.2 淺拷貝與深拷貝的區別?

3.3 淺拷貝產生的幾種情況

  • 使用切片 [:] 操作
  • 使用工廠函數(如 list/dir/set)
    工廠函數看上去像函數,實質上是類,調用時實際上是生成了該類型的一個實例,就像工廠生產貨物一樣
  • 使用 copy 模塊中的 copy() 函數

四.其他相關

4.1 關於 Python 的可變/不可變對象

  • **不可變對象,該對象所指向的內存中的值不能被改變。**當改變某個變量時候,由於其所指的值不能被改變,相當於把原來的值複製一份後再改變,這會開闢一個新的地址,變量再指向這個新的地址。

  • **可變對象,該對象所指向的內存中的值可以被改變。**變量(準確的說是引用)改變後,實際上是其所指的值直接發生改變,並沒有發生複製行爲,也沒有開闢新的出地址,通俗點說就是原地改變。

Python 中,基本數值類型(int和float)、字符串str、元組 tuple 都是不可變類型。而列表 list、字典dict、集合 set 是可變類型

4.2 不可變對象的深淺拷貝

不可變對象類型,沒有被拷貝的說法,即便是用深拷貝,查看 id 是一樣的,如果對其重新賦值,也只是新創建一個對象,替換掉舊的而已。

一句話就是,不可變類型,不管是深拷貝還是淺拷貝,地址值和拷貝後的值都是一樣的。

4.3 對於可變對象深淺拷貝

可變對象拷貝則:

  • = 號賦值:值相等,地址相等
  • copy 淺拷貝:值相等,地址不相等,對象的對象地址相等
  • deepcopy 深拷貝:值相等,地址不相等,對象的對象地址不相等

總結:

  • 賦值只是改變引用,原來對象數據發生變化,被賦值的 變量也會發送變化
  • 淺拷貝只拷貝一層,拷貝後爲兩個對立對象,但子對象仍爲引用,仍然指向原來的子對象
  • 深拷貝將所有對象數據對立,拷貝對象的對象,原對象的改變不會印象其他獨立對象(即遞歸賦值所有對象)。

【相關鏈接】

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