標籤:
在之前的文章 http://www.cnblogs.com/bitpeng/p/4748148.html 中,大家看到了ret.append(path) 和ret.append(path[:])的巨大差別。這和Python的對象機制有關。現在談談這個問題!
我們知道,Python有可變對象和不可變對象,他們的表現行為也迥然不同。先來幾個簡單的問題:
1 def foo1(arg):2 arg = 53 print(arg)4 5 x = 16 foo(x) # 輸出57 print(x) # 輸出1
1 def foo2(arg):2 arg.append(3)3 4 x = [1, 2]5 print(x) # 輸出[1, 2]6 foo(x)7 print(x) # 輸出[1, 2, 3]
1 def foo3(arg):2 arg = [3]3 4 x = [1, 2]5 print(x) # 輸出[1, 2]6 foo(x)7 print(x) # 輸出[1, 2]
一、Python參數問題
1.關於Python預設參數問題,我之前一篇博文有過描述。請參考這裡:http://www.cnblogs.com/bitpeng/p/4747765.html
2.關於Python的參數傳遞問題。原來接觸過C/C++ 的朋友,肯定想過,Python函數調用時,到底是值傳遞,還是引用傳遞。看到網上說的最多的是:對於不可變對象,是值傳遞;對於可變對象,是引用傳遞。可是我個人感覺,這個描述不是很準確,因為Python函數調用時,不管是可變對象,還是不可變對象,參數引用的都是實參。但是,既然為什麼都是引用,結果卻表現不同,這就和Python的對象有關。
在Python中,任何東西都是對象。
Python使用物件模型來儲存資料,任何類型的值都是一個對象。所有的python對象都有3個特徵:身份、類型和值。
身份:每一個對象都有自己的唯一的標識,可以使用內建函數id()來得到它。這個值可以被認為是該對象的記憶體位址。
類型:對象的類型決定了該對象可以儲存的什麼類型的值,可以進行什麼操作,以及遵循什麼樣的規則。type()函數來查看python 對象的類型。
值:對象表示的資料項目。
運算子is、is not就是通過id()的傳回值(即身份)來判定的,也就是看它們是不是同一個對象的“標籤”。
這裡有個很形象的例子:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables
根據這裡,我們可以知道,Python是通過名字來訪問對象。這和其他語言有很大的不同,比如在C中,你定義了變數(注意是定義不是聲明),那麼編譯後就一定會給變數分配記憶體,以後對該變數的讀寫就是通過該記憶體位址進行的。而Python中,只會給對象分配記憶體,然後再通過名稱來訪問該對象而已。所以,這就是為什麼Python名稱可以賦值給任何類型的原因(並不是真的賦值). 參數傳遞也一樣,只是用形參名稱來訪問實參所表示的對象。跟所謂的值傳遞、引用傳遞沒有任何關係。所以:在foo2()中,append()執行列表方法,當然也會影響實參;而賦值操作,只是將x綁定到另一個列表對象。這樣,原來的實參列表還是原來的,沒有變化,現在foo2()和foo3()應該能懂了吧。
關於Python參數調用的結論:
- Python函數不會替換調用參數所引用的對象。
- 對一個參數名重新賦值不會起任何作用。
- Python函數可以修改參數,如果這個參數是可變的。
- 在Python中沒有什麼是被隱式複製的。
- 在Python中函數調用時,不存在什麼所謂的值傳遞和引用傳遞,只是通過名稱(形參)來訪問對象(實參所代表的對象), 這和Python對象機制是一致的!
二、可變對象,不可變對象複製行為
1 >>> a = 1 2 >>> b = 1 3 >>> a is b 4 True 5 >>> import copy 6 >>> c = copy.deepcopy(a) 7 >>> c 8 1 9 >>> c is a10 True11 >>> s = "abc"12 >>> c = copy.deepcopy(s)13 >>> c14 ‘abc‘15 >>> c is s16 True
>>> a = "abc"; b = a; c = a[:]; d = copy.deepcopy(a)>>> a,b,c,d(‘abc‘, ‘abc‘, ‘abc‘, ‘abc‘)>>> a is b ;c is a; d is aTrueTrueTrue
看到了嗎?對於字元、字串、數值型,不管是賦值,切片,還是深度複製,他們都是同一個對象。但是對於元組呢?表現有所不同
1 >>> a = (1,2,[3,4]); b = a; c = a[:];d = copy.deepcopy(a)2 >>> b is a; c is a; d is a3 True4 True5 False6 >>> a[2][0] = 0;a,b,c,d7 ((1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [3, 4]))
顯然,對於元組並且包含可變元素時,切片和深度複製表現時不一樣的。
結論:
1、賦值:簡單地拷貝對象的引用,兩個對象的id相同。
2、淺拷貝:建立一個新的組合對象,這個新對象與原對象共用記憶體中的子物件。
3、深拷貝:建立一個新的組合對象,同時遞迴地拷貝所有子物件,新的組合對象與原對象沒有任何關聯。雖然實際上會共用不可變的子物件,但不影響它們的相互獨立性。
淺拷貝和深拷貝的不同僅僅是對組合對象來說,所謂的組合對象就是包含了其它對象的對象,如列表,類執行個體。而對於數字、字串以及其它“原子”類型,沒有拷貝一說,產生的都是原對象的引用。
可能有些朋友會有疑問,元組時不可變的,為什麼還可以給a[2][0]賦值呢。
>>> a = (1,2,[3,4])
>>> a[2] = [1,2,3]Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: ‘tuple‘ object does not support item assignment>>> a[2][:] = [1,2,3]>>> a(1, 2, [1, 2, 3])
這裡我們這樣認為,元組可以包含可變對象,只要元組的每個元素的id沒有變化即可。所以a[2] = [1,2,3]是嘗試把列表第3個元素引用其他的列表,id肯定變了;所以不支援。但是a[2][:] = [1,2,3]是原地賦值,雖然列表本身變了,但是列表本身的id號沒變,所以支援。
結論:對於不可變對象如元組:僅僅代表,他的每一個元素的id號是不可變的。如果元組本身包含可變元素,那麼還是可以改變他的值的!
三、Python與二維數組
之前做演算法題,需要用二維數組,所以很當然的想到了二維列表。
需求:初始化一個8行8列的數組,每個元素初始化為0.
當時,我想當然的是這樣做的:
>>> a = [[0] * 8 ]* 8; a[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
當時還想著,看,多完美!!!
結果程式總是死迴圈,不能正常退出。剛開始,結果對程式邏輯進行一次次的檢查後,問題還是無解!於是開始調試。最後發現了詭異的問題。
問:a[i][j] = 1 後,程式會發送什麼, 其中0 <= i, j < 8.
>>> a = [[0] * 8 ]* 8; a
[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
>>> a[0][0] = 1;a
[[1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0]]
呵呵,發現了嗎?給a[0][0]賦值後,結果很多其他的元素也都變了。現在看這個問題其實很簡單,因為列表執行乘法,相當於是淺複製。所以a中看似有8個列表,實際上引用的都是同一個。ps:這個問題,實際上書上講過,我忘了!結果寫程式時,出了這樣的問題.
>>> for i in a:... print id(a)... 140383550710200140383550710200140383550710200140383550710200140383550710200140383550710200140383550710200140383550710200
也許有人會問,那為什麼a = [[0] * 8 ]* 8; a[0][0] = 1後, 只有每一列第一個元素變為1,而其他的不變呢?這個問題也簡單:因為0是不可變對象。實際上,a = [[0] * 8 ]* 8執行完畢後,a的所有元素id都是相同的。但是,執行a[0][0] = 1後,a[0][0] 的id號已經變了。
>>> for i in a[0]:print id(i)... 3227684832276848322768483227684832276848322768483227684832276848
>>> a[0][0] = 1>>> for i in a[0]:print id(i)... 3227682432276848322768483227684832276848322768483227684832276848
結論:需要二維數組時,老老實實的用列表推導。
>>> a = [[0 for i in range(8)] for i in range(8)]>>> a[[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]>>> a[0][0] = 1>>> a[[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
現在已經一切正常了。
Python坑系列:可變對象與不可變對象