在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能夠找到這個外層列表包括其內元素。
下面是深、淺拷貝的概念:
所謂第一層次,指的是出現嵌套的複雜數據結構時,那些引用指向的數據對象屬於深一層次的數據。例如:
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])
通常來講,淺拷貝或按引用賦值就是咱們所期待的操做。只有少數時候(好比數據序列化、要傳輸、要持久化等),才須要深拷貝操做,但這些操做通常都內置在對應的函數中,無需咱們手動去深拷貝。