1. 程式人生 > >python動態型別與物件拷貝

python動態型別與物件拷貝

                                       python動態型別與物件拷貝

文章開始把我喜歡的這句話送個大家:這個世界上還有什麼比自己寫的程式碼執行在一億人的電腦上更酷的事情嗎,如果有那就是讓這個數字再擴大十倍。


1.動態型別和物件引用

在Python中,我們要明確一個概念:變數名和物件是劃分開的, 變數名永遠沒有任何關聯的型別資訊
,型別是和物件關聯的,而不存在於變數名中。一個變數名當第一次被賦值的時候被建立,而當新的賦值表示式出現時,他會馬上被當前新引用的物件所代替。這就是Python所謂的 動態型別機制

具體看一個例子:
程式碼片段:

a = 'abcde'
print(a)
a = [1,2,3,4,5]
print(a)
執行結果:

abcde
[1, 2, 3, 4, 5]
結合上面這個簡單的例子,我們再來從頭仔細理一理:

1、建立了一個字串物件’abcde’,然後建立了一個變數a,將變數a和字串物件 ‘abcde’相連線,
2、之後又建立了一個列表物件[1,2,3,4,5],然後又將他和a相連線。

這種從變數到物件的連線,我們稱之為引用,以記憶體中的指標形式實現。因此直白的說,在內部,變數事實上是到物件記憶體空間的一個指標,而且指向的物件可以隨著程式賦值語句而不斷變化。

總結一下:變數名沒有型別,只有物件才有型別,變數只是引用了不同型別的物件而已。每一個物件都包含了兩個頭部資訊,一個是型別標誌符,標識這個物件的型別,以及一個引用的計數器,用來表示這個物件被多少個變數名所引用,如果此時沒有變數引用他,那麼就可以回收這個物件。

2.Python垃圾收集機制

基於上面談到的引用機制,我們再說說Python的垃圾收集機制

還是上面那個例子,每當一個變數名被賦予了一個新的物件,那麼之前的那個物件佔用的空間就會被回收,前提是如果他沒有被其他變數名或者物件引用。這種自動回收物件空間的機制叫做垃圾收集機制。

即當a被賦值給列表物件[1,2,3,4,5]時, 字串物件的記憶體空間就被自動回收(前提是如果他沒有被別的變數引用

具體的內部機制是這樣的:Python在每個物件中儲存了一個計數器,計數器記錄了當前指向該物件的引用的數目。一旦這個計數器被設定為0,這個物件的記憶體空間就會自動回收。當a被賦值給列表物件後,原來的字串物件‘abcde’的引用計數器就會變為0,導致他的空間被回收。 這就使得我們不必像C++那樣需要專門編寫釋放記憶體空間的程式碼了


3.共享引用機制

我們接著再說說共享引用的內容,如下所示,多個變數名引用了同一個物件,稱為共享引用:
程式碼片段:

a = 'abcde'
b = a
print(a)
print(b)
執行結果:

abcde
abcde
此時字串物件’abcde’的引用計數是2,我們進一步往下看如果我們此時對變數a重新賦值呢?
程式碼片段:

a = 'abcde'
b = a
a = [1,2,3,4]
print(a)
print(b)
執行結果:

[1, 2, 3, 4]
Abcde
結果是顯而易見的,變數a變成了列表物件的引用,而變數b依然是字串物件’abcde’的引用,並且字串物件的引用計數為由2變為1.

如果此時再對b進行重新賦值,字串物件‘abcde’的引用計數就會變為0,然後這個物件就被垃圾回收了。

我們今天的話題要從“可變物件的原處修改”這裡引入,這是一個值得注意的問題。

4.可變物件的原處修改

正如剛才我們談到的,賦值操作總是儲存物件的引用,而不是這些物件的拷貝。由於在這個過程中賦值操作會產生相同物件的多個引用,因此我們需要意識到“可變物件”在這裡可能存在的問題:在原處修改可變物件可能會影響程式中其他引用該物件的變數。如果你不想看到這種情景,則你需要明確的拷貝一個物件,而不是簡單賦值。
程式碼片段:

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}
我不得不說,有坑請繞行,在這些地方還真的挺容易犯錯的。

引用是其他語言中指標的更高層的模擬。他可以幫助你在程式範圍內任何地方傳遞大型物件而不必在途中產生拷貝,起到優化程式的作用。

5.獲取物件的獨立拷貝

但是,如果我不想共享物件引用,而是想實實在在獲取物件的一份獨立的複製,該怎麼辦呢?這個需求在實際的程式設計中也很常見,常用的手法有以下幾種:

5.1.分片返回新的物件拷貝

第一種方法:分片表示式能返回一個新的物件拷貝,沒有限制條件的分片表示式能夠完全複製列表
程式碼片段:

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的值。

5.2.字典的copy方法可獲取獨立拷貝

第二種方法:字典的copy方法也能夠實現字典的完全複製:
程式碼片段:

D = {'a':1, 'b':2}
B = D.copy()
B['a'] = 888
print(B)
print(D)
執行結果:

{'a': 888, 'b': 2}
{'a': 1, 'b': 2}
5.3.內建函式list可以生成獨立拷貝

第三種:內建函式list可以生成拷貝
程式碼片段:

L = [1,2,3,4]
C = list(L)
C[0] = 888
print(C)
print(L)
執行結果:

[888, 2, 3, 4]
[1, 2, 3, 4]
5.4.應用舉例

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

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

L = [1,2,3,4]
A = [1,2,3,L]
B = A[:]
B[1] = 333
B[3] = ['888','999']//沒有引用L直接修改的B[3]
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']//引用了L
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方法只能進行頂層的賦值。就是在最頂層,如果是數值物件就複製數值,如果是物件引用就直接複製引用,所以仍然存在下一級潛藏的共享引用現象。
   5.5.deepcopy自頂向下遞迴獨立複製

如果想實現自頂向下,深層次的將每一個層次的引用都做完整獨立的複製,那麼就要使用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]
這樣,就實現了遞迴的遍歷物件來複制他所有的組成成分,實現了完完全全的拷貝,彼此之間再無瓜葛。

加油吧,程式設計師!