python中拷貝對象的區別

一、賦值、引用

在python中賦值語句總是建立對象的引用值,而不是複製對象。因此,python變量更像是指針,而不是數據存儲區域

這點和大多數語音類似吧,比如C++、Java等

 1、先看個例子:

values=[0,1,2]
values[1]=values
print(values)    # [0, [...], 2]

預想應該是:[0,[0,1,2],2],但結果卻爲何要賦值無限次?

可以說 Python 沒有賦值,只有引用。你這樣相當於創建了一個引用自身的結構,所以導致了無限循環。爲了理解這個問題,有個基本概念需要搞清楚。
Python 沒有「變量」,我們平時所說的變量其實只是「標籤」,是引用。
執行:values=[0,1,2]的時候,python做的事情是首先創建一個列表對象[0,1,2],然後給它貼上名爲values的標籤。如果隨後執行values=[3,4,5]
的話,python做的事情是創建另一個列表對象[3,4,5],然後把剛纔那張名爲values的標籤從前面的[0,1,2]對象上撕下來,重新貼到[3,4,5]這個對象上。

至始至終,並沒有一個叫做 values 的列表對象容器存在,Python 也沒有把任何對象的值複製進 values 去。過程如圖所示:

執行:values[1]=values的時候,python做的事情則是把values這個標籤所引用的列表對象的第二個元素指向values所引用的列表對象本身。執行完畢後,values標籤還是指向原來那個對象,只不過那個對象的結構發生了變化,從之前的列表[0,1,2]變成了[0,?,2],而這個?則是指向那個對象本身的一個引用。如圖所示:

values[:]   # 生成對象的拷貝或者是複製序列,不再是引用和共享變量,但此法只能頂層複製

所以你需要執行:values[1]=values[:]

Python 做的事情是,先 dereference 得到 values 所指向的對象 [0, 1, 2],然後執行 [0, 1, 2][:] 複製操作得到一個新的對象,內容也是 [0, 1, 2],然後將 values 所指向的列表對象的第二個元素指向這個複製二來的列表對象,最終 values 指向的對象是 [0, [0, 1, 2], 2]。過程如圖所示:

往更深處說,values[:] 複製操作是所謂的「淺複製」(shallow copy),當列表對象有嵌套的時候也會產生出乎意料的錯誤,比如

a=[0,[1,2],3]
b=a[:]
a[0]=8
a[1][1]=9
print(a)    # [8, [1, 9], 3]
print(b)    # [0, [1, 9], 3]

b 的第二個元素也被改變了。想想是爲什麼?不明白的話看下圖

正確的複製嵌套元素的方法是進行「深複製」(deep copy),方法是

import copy
 
a = [0, [1, 2], 3]
b = copy.deepcopy(a)
a[0] = 8
a[1][1] = 9

2、引用vs拷貝

(1)沒有限制條件的分片表達式(L[:])能夠複製序列,但此法只能淺層複製。

(2)字典 copy 方法,D.copy() 能夠複製字典,但此法只能淺層複製

(3)有些內置函數,例如 list,能夠生成拷貝 list(L)

(4)copy 標準庫模塊能夠生成完整拷貝:deepcopy 本質上是遞歸 copy

(5)對於不可變對象和可變對象來說,淺複製都是複製的引用,只是因爲複製不變對象和複製不變對象的引用是等效的(因爲對象不可變,當改變時會新建對象重新賦值)。所以看起來淺複製只複製不可變對象(整數,實數,字符串等),對於可變對象,淺複製其實是創建了一個對於該對象的引用,也就是說只是給同一個對象貼上了另一個標籤而已。

3、增強賦值以及共享引用

x = x + y,x 出現兩次,必須執行兩次,性能不好,合併必須新建對象 x,然後複製兩個列表合併

屬於複製/拷貝

x += y,x 只出現一次,也只會計算一次,性能好,不生成新對象,只在內存塊末尾增加元素。

當 x、y 爲list時, += 會自動調用 extend 方法進行合併運算,in-place change。

屬於共享引用

二、深拷貝deepcopy與淺拷貝copy

python中的對象之間賦值時是按引用傳送的,如果需要拷貝對象,需要使用標準庫中的copy模塊

1、copy.copy 淺拷貝,只拷貝父對象,不會拷貝對象的內部的子對象。(子對象(數組)修改,也會修改)

2、copy.deepcopy 深拷貝,拷貝對象及其子對象(原始對象)

import copy
a=[1,2,[3,4],{'a':1}]   # 原始對象
b=a     # 賦值,傳對象的引用
c=copy.copy(a)  # 對象拷貝,淺拷貝
d=copy.deepcopy(a)  # 對象拷貝,深拷貝
e=a[:]  # 能複製序列,淺拷貝

a.append('add1')  # 修改對象a
a[2].append('add2')  # 修改對象a中的[3,4]數組對象
a[3]='666'
print('a:',a)
print('b:',b)
print('c:',c)
print('d:',d)
print('e:',e)
"""
執行結果:
a: [1, 2, [3, 4, 'add2'], '666', 'add1']b: [1, 2, [3, 4, 'add2'], '666', 'add1']c: [1, 2, [3, 4, 'add2'], {'a': 1}]d: [1, 2, [3, 4], {'a': 1}]e: [1, 2, [3, 4, 'add2'], {'a': 1}]
解釋:
copy.copy 淺拷貝 只拷貝父對象,不會拷貝對象的內部的子對象。子對象(數組)修改,也會修改     copy.deepcopy 深拷貝 拷貝對象及其子對象(原始對象)
"""

三、深入理解python變量作用域及其陷阱

1、可變對象&不可變對象

在Python中,對象分爲兩種:可變對象和不可變對象,不可變對象包括int,float,long,str,tuple等,可變對象包括list,set,dict等。需要注意的是:這裏說的不可變指的是值的不可變。對於不可變類型的變量,如果要更改變量,則會創建一個新值,把變量綁定到新值上,而舊值如果沒有被引用就等待垃圾回收。另外,不可變的類型可以計算hash值,作爲字典的key。可變類型數據對對象操作的時候,不需要再在其他地方申請內存,只需要在此對象後面連續申請(+/-)即可,也就是它的內存地址會保持不變,但區域會變長或者變短。

a='hello'
print(id(a))    # 1991608735200
a='python'
print(id(a))    # 1991608735368
 
# 重新賦值之後,變量a的內存地址已經變了
# 'hello'是str類型,不可變,所以賦值操作知識重新創建了str 'python'對象,然後將變量a指向了它
 
 
l1=[1,2,3]
print(id(l1))   # 2262493958280
l1.append(4)
print(id(l1))   # 2262493958280
 
# list重新賦值之後,變量l1的內存地址並未改變
# [1, 2, 3]是可變的,append操作只是改變了其value,變量l1指向沒有變

2、函數值傳遞

def func_int(a):
    a+=4

def func_list(l1):
    l1[0]=4

t=0
func_int(t)
print(t)    # 0

t_list=[1,2,3]
func_list(t_list)
print(t_list)   # [4, 2, 3]

對於上面的輸出,不少Python初學者都比較疑惑:第一個例子看起來像是傳值,而第二個例子確實傳引用。其實,解釋這個問題也非常容易,主要是因爲可變對象和不可變對象的原因:對於可變對象,對象的操作不會重建對象,而對於不可變對象,每一次操作就重建新的對象。

    在函數參數傳遞的時候,Python其實就是把參數裏傳入的變量對應的對象的引用依次賦值給對應的函數內部變量。參照上面的例子來說明更容易理解,func_int中的局部變量"a"其實是全部變量"t"所指向對象的另一個引用,由於整數對象是不可變的,所以當func_int對變量"a"進行修改的時候,實際上是將局部變量"a"指向到了整數對象"1"。所以很明顯,func_list修改的是一個可變的對象,局部變量"a"和全局變量"t_list"指向的還是同一個對象。

 

3、陷阱:使用可變的默認參數

我多次見到過如下的代碼:
def foo(a, b, c=[]):
# append to c
# do some more stuff
永遠不要使用可變的默認參數,可以使用如下的代碼代替:
def foo(a, b, c=None):
    if c is None:
        c = []
    # append to c
    # do some more stuff
‍‍與其解釋這個問題是什麼,不如展示下使用可變默認參數的影響:‍‍
In[2]: def foo(a, b, c=[]):
...        c.append(a)
...        c.append(b)
...        print(c)
...
In[3]: foo(1, 1)
[1, 1]
In[4]: foo(1, 1)
[1, 1, 1, 1]
In[5]: foo(1, 1)
[1, 1, 1, 1, 1, 1]
同一個變量c在函數調用的每一次都被反覆引用。這可能有一些意想不到的後果。

 

參考:

https://www.cnblogs.com/jiangzhaowei/p/5740913.html

https://www.cnblogs.com/shenbuer/p/7977323.html

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