python 賦值、深淺拷貝、作用域

python中的賦值語句

python中關於對象複製有三種類型的使用方式,賦值、淺拷貝與深拷貝。在 python 中賦值語句總是建立對象的引用值,而不是複製對象。因此,python 變量更像是指針,而不是數據存儲區域。如下圖所示:


>>> list_a = [1,2,3,"hello",[4,5]]
>>> list_b = list_a
>>> list_b
[1, 2, 3, 'hello', [4, 5]]
>>> list_b is list_a
True
>>> id(list_a)
30787784
>>> id(list_b)
30787784
如上面代碼,把list_a賦值給list_b,意味着兩個指向同一個內存空間,通過查看兩個list值和id發現兩者相同。賦值操作(包括對象作爲參數、返回值)不會開闢新的內存空間,它只是複製了新對象的引用。也就是說,除了list_b這個名字以外,沒有其它的內存開銷。修改了list_a,就影響了list_b;同理,修改了list_b就影響了list_a。

簡單偏題下關於id的一個問題:

>>> a = 2.5
>>> b = 2.5
>>> id(a)
19603848
>>> id(b)
30973856
>>> a is b
False
>>> a = 2
>>> b = 2
>>> a is b
True
>>> id(a)
19571404
>>> id(b)
19571404
可以得到一個簡單的結論就是:解釋器在對值很小的int和很短的字符串的時候做了一點小優化,只分配了一個對象,讓它們id一樣了。

參考:http://stackoverflow.com/questions/3402679/identifying-objects-why-does-the-returned-value-from-id-change
先看下有個比較有趣例子:

>>> values = [1,2,3]
>>> values[1] = values
>>> values
[1, [...], 3]
把list中第二個元素賦值爲本身,本以爲結果是[1,[1,2,3],3] ,發現輸出結果是循環無限次,這裏需要明確的一個概念是:Python 沒有賦值,只有引用。你這樣相當於創建了一個引用自身的結構,所以導致了無限循環。
>>> values = [0,1,2]
>>> id(values)
19149888
>>> values = [3,4,5]
>>> id(values)
30779992

Python 做的事情是首先創建一個列表對象 [0, 1, 2],然後給它貼上名爲 values 的標籤。如果隨後又執行 Python 做的事情是創建另一個列表對象 [3, 4, 5],然後把剛纔那張名爲 values 的標籤從前面的 [0, 1, 2] 對象上撕下來,重新貼到 [3, 4, 5] 這個對象上。 至始至終,並沒有一個叫做 values 的列表對象容器存在,Python 也沒有把任何對象的值複製進 values 去。過程如圖所示: 



當執行values[1]=values,Python 做的事情則是把 values 這個標籤所引用的列表對象的第二個元素指向 values 所引用的列表對象本身。如下圖:


從上圖可以發現當要打印values 就會陷入死循環。

那麼假如我需要使得values[1] 值就爲[0,1,2] ,這時就需要涉及到數據拷貝問題。

python 深淺拷貝

淺拷貝

淺拷貝有三種形式:切片操作,工廠函數,copy模塊中的copy函數

>>> values = [0,1,2]
>>> id(values)
30779592
>>> values[1] = values[:]
>>> id(values)
30779592

上述代碼能使得values[1] 值就爲[0,1,2],Python 做的事情是,先 dereference 得到 values 所指向的對象 [0, 1, 2],然後執行 [0, 1, 2][:] 複製操作得到一個新的對象,內容也是 [0, 1, 2],然後將 values 所指向的列表對象的第二個元素指向這個複製二來的列表對象,最終 values 指向的對象是 [0, [0, 1, 2], 2]。 

>>> values = [0,1,2]
>>> id(values)
30779352
>>> id(values[1])
19571416
>>> values[1] = values[:]
>>> id(values[1])
30779712
>>> values
[0, [0, 1, 2], 2]

改變values[1] 後其id值發生改變過程如下圖:


除了上面切片方法,還可以使用工廠函數方法:

>>> values = [0,1,2]
>>> id(values)
30779792
>>> id(values[1])
19571416
>>> values[1] = list(values)
>>> values
[0, [0, 1, 2], 2]
>>> id(values)
30779792
>>> id(values[1])
30440224

第三種方法使用copy模塊的copy函數:

>>> import copy
>>> values = [0,1,2]
>>> id(values)
30779352
>>> id(values[1])
19571416
>>> values[1] = copy.copy(values)
>>> values
[0, [0, 1, 2], 2]
>>> id(values)
30779352
>>> id(values[1])
30774976

前面說了三種淺拷貝的方式,但是當list中出現嵌套結構時,會發生出乎意料的錯誤;

>>> a = [0,[1,2],3]
>>> b = a[:]
>>> id(a)
30777552
>>> id(b)
30780192
>>> id(a[1])
24241536
>>> id(b[1])
24241536

使用前面所說的切片淺拷貝,我們發現a,b 的id地址不同,但是a[1] b[1] 地址相同,如下圖所示:


所以當我們對a[1] 或者b[1] 其中一個進行修改時,兩個結構都會改變,可能我們不需要這種情況發生。

深拷貝

深拷貝在python中只有一種方法,copy模塊中的deepcopy函數,deepcopy 本質上是遞歸 copy

和淺拷貝對應,深拷貝拷貝了對象的所有元素,包括多層嵌套的元素。因而,它的時間和空間開銷要高。

>>> a = [0,[1,2],3]
>>> b = copy.deepcopy(a)
>>> id(a)
30779992
>>> id(b)
30779792
>>> id(a[1])
30774376
>>> id(b[1])
30780472

當進行深拷貝我們發現,a[1] b[1] 地址空間不相同了,如下圖所示:


所以這時我們對a 的任何修改將不會影響到 b。

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

L = [1, 2, 3]
D = {'a':1, 'b':2}
A = L[:]
B = D.copy()
print "L, D"
print  L, D
print "A, B"
print A, B
print "--------------------"
A[1] = 'NI'
B['c'] = 'spam'
print "L, D"
print  L, D
print "A, B"
print A, B
 
 
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 2, 3] {'a': 1, 'b': 2}
--------------------
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 'NI', 3] {'a': 1, 'c': 'spam', 'b': 2}

增強賦值與共享引用

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

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

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

>>> L = [1,2]
>>> M = L
>>> id(M)
30774616
>>> id(L)
30774616
>>> L = L + [3,4]
>>> id(L)
24241336
>>> print L,M
[1, 2, 3, 4] [1, 2]

L = L + [3,4]這時會重新把L 指向 L + [3,4] 產生新對象地址,所以L id值發生改變

>>> L = [1,2]
>>> id(L)
30440224
>>> M = L
>>> id(M)
30440224
>>> L += [3,4]
>>> id(L)
30440224
>>> id(M)
30440224
>>> print L,M
[1, 2, 3, 4] [1, 2, 3, 4]

這裏調用 +=不會產生新的臨時對象更加高效。

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

可變對象與不可變對象

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

>>> str = "python"
>>> id(str)
19392416
>>> str = "java"
>>> id(str)
19393696

str類型不可改變,當str重新賦值後地址發生改變。

>>> list_a = [1,2,3]
>>> id(list_a)
30780352
>>> list_a.append(4)
>>> id(list_a)
30780352

list後面增加數據但是list 地址沒有改變。

函數傳值

>>> def func_int(a):
	print id(a)
	a += 4
	print id(a)

	
>>> t = 0
>>> func_int(t)
19571428
19571380
>>> t
0
>>> id(t)
19571428

因爲int 型數據不可該表,當在函數中對a+=4 操作a 指向了另外值爲4的地址。

>>> def func_list(list_type):
	print id(list_type)
	list_type[0] = 4
	print id(list_type)

	
>>> list_type = [1,2,3]
>>> func_list(list_type)
19149888
19149888
>>> list_type
[4, 2, 3]
>>> id(list_type)
19149888

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

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

參考博客:http://my.oschina.net/leejun2005/blog/145911?fromerr=4HCQGjkP




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