按引用賦值而不是拷貝副本
在python中,無論是直接的變量賦值,還是參數傳遞,都是按照引用進行賦值的。
在計算機語言中,有兩種賦值方式:按引用賦值、按值賦值。其中按引用賦值也常稱為按指針傳值(當然,它們還是有點區別的),后者常稱為拷貝副本傳值。它們的區別,詳細內容參見:按值傳遞 vs. 按指針傳遞。
下面僅解釋python中按引用賦值的相關內容,先分析下按引用賦值的特別之處,然后分析按引用賦值是什么樣的過程。
按引用賦值的特性
例如:
a = 10000
b = a
>>> a,b
(10000, 10000)
這樣賦值后,b和a不僅在值上相等,而且是同一個對象,也就是說在堆內存中只有一個數據對象10000,這兩個變量都指向這一個數據對象。從數據對象的角度上看,這個數據對象有兩個引用,只有這兩個引用都沒了的時候,堆內存中的數據對象10000才會等待垃圾回收器回收。
它和下面的賦值過程是不等價的:
a = 10000
b = 10000
雖然a和b的值相等,但他們不是同一個對象,這時候在堆內存中有兩個數據對象,只不過這兩個數據對象的值相等。
對於不可變對象,修改變量的值意味着在內存中要新創建一個數據對象。例如:
a = 10000
b = a
a = 20000
>>> a,b
(20000, 10000)
在a重新賦值之前,b和a都指向堆內存中的同一個數據對象,但a重新賦值后,因為數值類型10000是不可變對象,不能在原始內存塊中直接修改數據,所以會新創建一個數據對象保存20000,最后a將指向這個20000對象。這時候b仍然指向10000,而a則指向20000。
結論是:對於不可變對象,變量之間不會相互影響。正如上面重新賦值了a=20000,但變量b卻沒有任何影響,仍然指向原始數據10000。
對於可變對象,比如列表,它是在"原處修改"數據對象的(注意加了雙引號)。比如修改列表中的某個元素,列表的地址不會變,還是原來的那個內存對象,所以稱之為"原處修改"。例如:
L1 = [111,222,333]
L2 = L1
L1[1] = 2222
>>> L1,L2
([111, 2222, 333], [111, 2222, 333])
在L1[1]賦值的前后,數據對象[111,222,333]的地址一直都沒有改變,但是這個列表的第二個元素的值已經改變了。因為L1和L2都指向這個列表,所以L1修改第二個元素后,L2的值也相應地到影響。也就是說,L1和L2仍然是同一個列表對象[111,2222,333]。
結論是:對於可變對象,變量之間是相互影響的。
按引用賦值的過程分析
當將段數據賦值給一個變量時,首先在堆內存中構建這個數據對象,然后將這個數據對象在內存中的地址保存到棧空間的變量中,這樣變量就指向了堆內存中的這個數據對象。
例如,a = 10賦值后的圖示:

如果將變量a再賦值給變量b,即b = a,那么賦值后的圖示:

因為a和b都指向對內存中的同一個數據對象,所以它們是完全等價的。這里的等價不僅僅是值的比較相等,而是更深層次的表示同一個對象。就像a=20000和c=20000,雖然值相等,但卻是兩個數據對象。這些內容具體的下一節解釋。
在python中有可變數據對象和不可變數據對象的區分。可變的意思是可以在堆內存原始數據結構內修改數據,不可變的意思是,要修改數據,必須在堆內存中創建另一個數據對象(因為原始的數據對象不允許修改),並將這個新數據對象的地址保存到變量中。例如,數值、字符串、元組是不可變對象,列表是可變對象。
可變對象和不可變對象的賦值形式雖然一樣,但是修改數據時的過程不一樣。
對於不可變對象,修改數據是直接在堆內存中新創建一個數據對象。如圖:

對於可變對象,修改這個可變對象中的元素時,這個可變對象的地址不會改變,所以是"原處修改"的。但需要注意的是,這個被修改的元素可能是不可變對象,可能是可變對象,如果被修改的元素是不可變對象,就會創建一個新數據對象,並引用這個新數據對象,而原始的那個元素將等待垃圾回收器回收。
>>> L=[333,444,555]
>>> id(L),id(L[1])
(56583832, 55771984)
>>> L[1]=4444
>>> id(L),id(L[1])
(56583832, 55771952)
如圖所示:

早就存在的小整數
數值對象是不可變對象,理論上每個數值都會創建新對象。
但實際上並不總是如此,對於[-5,256]這個區間內的小整數,因為python內部引用過多,這些整數在python運行的時候就事先創建好並編譯好對象了。所以,a=2, b=2, c=2根本不會在內存中新創建數據對象2,而是引用早已創建好的初始化數值2。
所以:
>>> a=2
>>> b=2
>>> a is b
True
其實可以通過sys.getrefcount()函數查看數據對象的引用計數。例如:
>>> sys.getrefcount(2)
78
>>> a=2
>>> sys.getrefcount(2)
79
對於小整數范圍內的數的引用計數都至少是幾十次的,而超出小整數范圍的數都是2或者3(不同執行方式得到的計數值不一樣,比如交互式、文件執行)。
對於超出小整數范圍的數值,每一次使用數值對象都創建一個新數據對象。例如:
>>> a=20000
>>> b=20000
>>> a is b
False
因為這里的20000是兩個對象,這很合理論。但是看下面的:
>>> a=20000;b=20000
>>> a is b
True
>>> a,b=20000,20000
>>> a is b
True
為什么它們會返回True?原因是python解析代碼的方式是按行解釋的,讀一行解釋一行,創建了第一個20000時發現本行后面還要使用一個20000,於是b也會使用這個20000,所以它返回True。而前面的換行賦值的方式,在解釋完一行后就會立即忘記之前已經創建過20000的數據對象,於是會為b創建另一個20000,所以它返回False。
如果是在python文件中執行,則在同意作用域內的a is b一直都會是True,而不管它們的賦值方式如何。這和代碼塊作用域有關:整個py文件是一個模塊作用域。此處只給測試結果,不展開解釋,否則篇幅太大了,如不理解下面的結果,可看我的另一篇Python作用域詳述。
a = 25700
b = 25700
print(a is b) # True
def f():
c = 25700
d = 25700
print(c is d) # True
print(a is c) # False
f()
深拷貝和淺拷貝
對於下面的賦值過程:
L1 = [1,2,3]
L2 = L1
前面分析過修改L1或L2的元素時都會影響另一個的原因:按引用賦值。實際上,按引用是指直接將L1中保存的列表內存地址拷貝給L2。
再看一個嵌套的數據結構:
L1 = [1,[2,22,222],3]
L2 = L1
這里從L1拷貝給L2的也是外層列表的地址,所以L2可以找到這個外層列表包括其內元素。
下面是深、淺拷貝的概念:
- 淺拷貝:shallow copy,只拷貝第一層的數據。python中賦值操作或copy模塊的copy()就是淺拷貝
- 深拷貝:deep copy,遞歸拷貝所有層次的數據,python中copy模塊的deepcopy()是深拷貝
所謂第一層次,指的是出現嵌套的復雜數據結構時,那些引用指向的數據對象屬於深一層次的數據。例如:
L = [2,22,222]
L1 = [1,2,3]
L2 = [1,L,3]
L和L1都只有一層深度,L2有兩層深度。淺拷貝時只拷貝第一層的數據作為副本,深拷貝遞歸拷貝所有層次的數據作為副本。
例如:
>>> L=[2,22,222]
>>> L1=[1,L,3]
>>> L11 = copy.copy(L1)
>>> L11,L1
([1, [2, 22, 222], 3], [1, [2, 22, 222], 3])
>>> L11 is L1
False
>>> id(L1),id(L11) # 不相等
(17788040, 17786760)
>>> id(L1[1]),id(L11[1]) # 相等
(17787880, 17787880)
注意上面的L1和L11是不同的列表對象,但它們中的第二個元素是同一個對象,因為copy.copy是淺拷貝,只拷貝了這個內嵌列表的地址。
而深拷貝則完全創建新的副本對象:
>>> L111 = copy.deepcopy(L1)
>>> L1[1],L111[1]
([2, 22, 222], [2, 22, 222])
>>> id(L1[1]),id(L111[1])
(17787880, 17787800)
因為是淺拷貝,對於內嵌了可變對象的數據時,修改內嵌的可變數據,會影響其它變量。因為它們都指向同一個數據對象,這和按引用賦值是同一個道理。例如:
>>> s = [1,2,[3,33,333,3333]]
>>> s1 = copy.copy(s)
>>> s1[2][3] = 333333333
>>> s[2], s1[2]
([3, 33, 333, 333333333], [3, 33, 333, 333333333])
一般來說,淺拷貝或按引用賦值就是我們所期待的操作。只有少數時候(比如數據序列化、要傳輸、要持久化等),才需要深拷貝操作,但這些操作一般都內置在對應的函數中,無需我們手動去深拷貝。
