標籤:

關於python中「賦值就是建立一個對象的引用」,大家怎麼看?Python一切皆為對象又是什麼意思?

為什麼我覺得好不方便啊......

比如如下的代碼:

x=[1,2,3,4]
y=x
y[0]=4
print x
&>&> x = [4 2 3 4]


挺方便的

object.h

而Python中一切皆來源於此,而這兩個宏定義為:

其實可發現PyObject_VAR_HEAD也只是PyObject_HEAD加上一個ob_size,於是Python中,每一個對象都擁有相同的對象頭部,於是我們只需要用一個PyObject *就可以引用任意的一個對象,而不論該對象實際是一個什麼對象,所以,當內存中存在某個Python對象時,該對象的開始的幾個位元組的含義一定會符合我們的預期,即PyObject_HEAD。而PyObject_HEAD宏定義中的_PyObject_HEAD_EXTRA其實只是指向_object的一個雙向鏈表,

而ob_refcnt則是引用計數的計數器,而struct _typeobject *obtype則是類型,表示對象類型的類型對象:

通過PyObject_VAR_HEAD可以發現類型其實也是一個對象(一切皆對象),而類型對象的類型則是類型對象所關聯的類型:PyType_Type:

而即使如簡單的int也是來填滿這裡的東西:

於是,即使是int也是對象

所以即使你使用

i = 47

j = 47

也是對47的一個引用,而

通過id,可以看出其在內存引用位置相同,而為什麼相同?因為為了減少頻繁調用開銷,使用了small int對象池的技術:

於是只要超過257(包括257),則變了:

所以, m,n雖然都是對258對象的引用,卻是不同的內存地址了.

剩下的你就可以繼續按照這條路線探尋Python的機制了,而你的問題也基本得到解答了,當你疑惑一些問題時,源碼是最好的解釋。 :-)

P.S. Python版本: 2.7.8


變數一般有三種風格:

引用式:變數是對象的名字,在運行時可以綁定到任意對象上,比如python。

值式:變數是存儲位置的名字,在編譯時綁定已經完成,運行時不可以改變,比如c。

混合式:某些類型是值式的,某些類型是引用式的,比如c#的struct和class。

c++比較特殊,基本上可以說是值式的,但是由於有別名存在,所以也有些引用式的特點。

引用式和值式的區別就是在賦值操作上,引用式的賦值是變數名的重新綁定,而值式的賦值是對象的拷貝。

他們兩者是可以互相模擬的。引用式的想要有值式的賦值,只需要顯式的拷貝或者copy-on-write就可以了。而值式的想要有引用式的賦值只需要使用指針就可以了。

你可能只是習慣了c/c++的值式的風格,還沒適應引用式的風格。

看起來可能風格統一比較好。但是實際用起來我覺得c#的混合式是最好用的。


其實現在大部分的語言都是這樣的:對象以引用的方式提供給編程者,對象賦值只是多個變數指向同一個對象。

大概只有C/C++的賦值是真的拷貝一份。


a = 258

在Python看來就是:

創建一個PyIntObject對象,值為258;a是一個指向PyObject的指針,將a指向此PyIntObject對象

之所以能這麼干就和 @藍色的答案解釋的一樣,所有的PyObject都有同樣的頭部。


在Python中有些對象是可以在原處進行改變的(即可變對象),這種對象包括了列表、字典、集合和一些自定義的對象。而對於整數和字元串等不可變對象是不會存在題主所說的問題。比如:

&>&>&> a = 123
&>&>&> b = a
&>&>&> a = 321
&>&>&> print a, b
321 123

對於不可變對象進行賦值時只能重新開闢一塊新的內存來生成新的對象,而不能直接更改原對象。當然從內部來開,作為一種優化,python會緩存一些不變的對象並對其進行復用,例如小的整數和字元串。但是從邏輯的角度來看,這工作起來就像每一個表達式結果的值都是一個不同的對象,而每一個對象都是不同的內存。

對於樓主所說的問題:

x = [1, 2, 3, 4]
y = x
y[0] = 4
print x
&>&> x = [4 2 3 4]

其實即使在C語言中 仍然表現是類似的,也不能直接複製,比如:

int matrix_a[4] = {1, 2, 3, 4};
int *matrix_b =matrix_a;
*matrix_b[0] = 0;

此時matrix_a的值也會發生變化,CPython的地層實現就是C的指針,所以有相同的表現也不足為怪。

那我們想想為什麼不設計成直接複製,而採用引用相同對象的機制呢?我么平時其實很少在主幹程序中進行複製列表,字典這樣的變數,畢竟複製開銷太大。我們複製的對象主要是一些整數,字元串等小的不可變對象,所以Python對於可變對象賦值採用複製。對列表,字典這樣的可變對象賦值採用引用。而且這種引用在函數調用的參數中更加常見,你既然把一個列表作為參數大部分是想改變這個列表中的某個元素,即便不想改變,採用引用也是沒有任何壞處的。所以對於這些不可變對象還是使用引用更好。

那如果我們就想使用複製的方式處理這些列表、字典呢?也有辦法,像 @miG Lu 所說的,你可以使用切片或者內置列表函數來處理這種情況。但是切片只能用在序列對象上,所以字典和集合就不行了,那怎麼辦?用copy.copy(x) 和copy.deepcopy(x) !至於這兩者有何區別,我就不再這裡談了,查手冊吧!

另外,如果你對我說的話還是不能理解,我覺得你可以看看Learning Python這本書,夯實一下基礎,如果你已經看懂了,希望再深挖一下Python對象的實現機制,推薦你Python源碼剖析,國內不多的好書。

祝每個思考的人都能進步!


問題的關鍵是,你寫下y=x時,其實並沒有新建一個y,而是一個類似c++中引用的機制,具體可以這樣看:

&>&>&> x = [1,2,3,4]

&>&>&> y = x

&>&>&> id(x)

43246536L

&>&>&> id(y)

43246536L

所以x和y其實是一個東西。

如果要複製一個list,需要使用

&>&>&> y = list(x)

或者

&>&>&> y = x[:]


id()


你加上「指針」一起理解。


來,我來用我專欄里的一篇文章來回答。

專欄鏈接:給妹子講python,歡迎大家關注,提意見!

我從「可變對象的原處修改」這裡引入,這是一個值得注意的問題。

上一小節我們談到,賦值操作總是存儲對象的引用,而不是這些對象的拷貝。由於在這個過程中賦值操作會產生相同對象的多個引用,因此我們需要意識到「可變對象」在這裡可能存在的問題:在原處修改可變對象是可能會影響程序中其他引用該對象的變數。如果你不想看到這種情景,則你需要明確的拷貝一個對象,而不是簡單賦值。

X = [1,2,3,4,5]
L = [a, X, b]
D = {x:X, y:2}

print(L)
print(D)

[a, [1, 2, 3, 4, 5], b]
{y: 2, x: [1, 2, 3, 4, 5]}

在這個例子中,我們可以看到列表[1,2,3,4,5]有三個引用,被變數X引用、被列表L內部元素引用、被字典D內部元素引用,那麼利用這三個引用中的任意一個去修改列表[1,2,3,4,5],也會同時改變另外兩個引用的對象,例如我利用L來改變[1,2,3,4,5]的第二個元素,運行的結果就非常明顯。

X = [1,2,3,4,5]
L = [a, X, b]
D = {x:X, y:2}

L[1][2] = changed
print(X)
print(L)
print(D)

[1, 2, changed, 4, 5]
[a, [1, 2, changed, 4, 5], b]
{x: [1, 2, changed, 4, 5], y: 2}

引用是其他語言中指針的更高層的模擬。他可以幫助你在程序範圍內任何地方傳遞大型對象而不必在途中產生拷貝。

可是如果我不想共享對象引用,而是想實實在在獲取對象的一份獨立的複製,該怎麼辦呢?

能想到這一層確實很不錯,其實這個很簡單,常用的手法有以下幾種:

第一種方法:分片表達式能返回一個新的對象拷貝,沒有限制條件的分片表達式能夠完全複製列表

L = [1,2,3,4,5]
C = L[1:3]
C[0] = 8
print(C)
print(L)

[8, 3]
[1, 2, 3, 4, 5]

L = [1,2,3,4,5]
C = L[:]
C[0] = 8
print(C)
print(L)

[8, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

可以看出,用分片表達式得到了新的列表拷貝C,對這個列表進行修改,不會改變原始列表L的值。

第二種方法:字典的copy方法也能夠實現字典的完全複製:

D = {a:1, b:2}
B = D.copy()
B[a] = 888
print(B)
print(D)

{a: 888, b: 2}
{a: 1, b: 2}

第三種:內置函數list可以生成拷貝

L = [1,2,3,4]
C = list(L)
C[0] = 888
print(C)
print(L)

[888, 2, 3, 4]
[1, 2, 3, 4]

最後我們看一個複雜一些的例子

B通過無限制條件的分片操作得到了A列表的拷貝,B對列表內元素本身的修改,不會影響到A,例如修改數值,例如把引用換成別的列表引用:

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3] = [888,999]
print(B)
print(A)
print(L)

[1, 333, 3, [888, 999]]
[1, 2, 3, [1, 2, 3, 4]]
[1, 2, 3, 4]

但是如果是這種場景呢?

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3][1] = [changed]
print(B)
print(A)
print(L)

[1, 333, 3, [1, [changed], 3, 4]]
[1, 2, 3, [1, [changed], 3, 4]]
[1, [changed], 3, 4]

因為B的最後一個元素也是列表L的引用(可以看做獲取了L的地址),因此通過這個引用對所含列表對象元素進行進一步的修改,也會影響到A,以及L本身

所以說,無限制條件分片以及字典的copy方法只能進行頂層的賦值。就是在最頂層,如果是數值對象就複製數值,如果是對象引用就直接複製引用,所以仍然存在下一級潛藏的共享引用現象。

如果想實現自頂向下,深層次的將每一個層次的引用都做完整獨立的複製,那麼就要使用copy模塊的deepcopy方法。

import copy
L = [1,2,3,4]
A = [1,2,3,L]
B = copy.deepcopy(A)

B[3][1] = [changed]
print(B)
print(A)
print(L)

[1, 2, 3, [1, [changed], 3, 4]]
[1, 2, 3, [1, 2, 3, 4]]
[1, 2, 3, 4]

這樣,就實現了遞歸的遍歷對象來複制他所有的組成成分,實現了完完全全的拷貝,彼此之間再無瓜葛。

沒想到簡單的賦值還有這麼多的坑!最後再來總結總結:普通的=賦值得到的其實僅僅是共享引用;無限條件的分片、字典copy方法和內置函數list這三種方法可以進行頂層對象的拷貝,而deepcopy可以徹底的實現自頂向下的完全拷貝。


你想深拷貝就用y=x[:]。

其實也沒啥不好的,py的對象分為可變和不可變兩種,只要理解了這一點一切都很清晰。


推薦閱讀:

Python從零開始系列連載(1)——安裝環境
python寫的CGI腳本,用print為什麼不是列印到控制台,而是發送到客戶端?
跟黃哥學Python爬蟲抓取代理IP。
爆款遊戲《貪吃蛇大作戰》的 Python 實現

TAG:Python |