python按引用賦值和深、淺拷貝

按引用賦值而不是拷貝副本

在python中,不管是直接的變量賦值,仍是參數傳遞,都是按照引用進行賦值的html

在計算機語言中,有兩種賦值方式:按引用賦值、按值賦值。其中按引用賦值也常稱爲按指針傳值(固然,它們仍是有點區別的),後者常稱爲拷貝副本傳值。它們的區別,詳細內容參見:按值傳遞 vs. 按指針傳遞python

下面僅解釋python中按引用賦值的相關內容,先分析下按引用賦值的特別之處,而後分析按引用賦值是什麼樣的過程。數據結構

按引用賦值的特性

例如:函數

a = 10000
b = a

>>> a,b
(10000, 10000)

這樣賦值後,b和a不只在值上相等,並且是同一個對象,也就是說在堆內存中只有一個數據對象10000,這兩個變量都指向這一個數據對象。從數據對象的角度上看,這個數據對象有兩個引用,只有這兩個引用都沒了的時候,堆內存中的數據對象10000纔會等待垃圾回收器回收。測試

它和下面的賦值過程是不等價的:指針

a = 10000
b = 10000

雖然a和b的值相等,但他們不是同一個對象,這時候在堆內存中有兩個數據對象,只不過這兩個數據對象的值相等。code

對於不可變對象,修改變量的值意味着在內存中要新建立一個數據對象。例如:htm

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。blog

對於可變對象,好比列表,它是在"原處修改"數據對象的(注意加了雙引號)。好比修改列表中的某個元素,列表的地址不會變,仍是原來的那個內存對象,因此稱之爲"原處修改"。例如:

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])

通常來講,淺拷貝或按引用賦值就是咱們所期待的操做。只有少數時候(好比數據序列化、要傳輸、要持久化等),才須要深拷貝操做,但這些操做通常都內置在對應的函數中,無需咱們手動去深拷貝。

相關文章
相關標籤/搜索