Python 中對於變量的處理與 C 語言有着很大的不一樣,Python 中的變量具備一個特殊的屬性:identity,即「身份標識」。這種特殊的屬性也在不少地方被稱爲「引用」。html
爲了更加清晰地說明引用相關的問題,咱們首先要介紹兩個工具:一個Python的內置函數:id()
;一個運算符:is
;同時還要介紹一個sys
模塊內的函數:getrefcount()
。python
id()
id(object)
編程
Return the 「identity」 of an object. This is an integer which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same `id()`[1] value.數據結構
返回值爲傳入對象的「標識」。該標識是一個惟一的常數,在傳入對象的生命週期內與之一一對應。生命週期沒有重合的兩個對象可能擁有相同的
id()
返回值。app
CPython implementation detail: This is the address of the object in memory.ide
CPython 實現細節:「標識」實際上就是對象在內存中的地址。函數
——引自《Python 3.7.4 文檔-內置函數-id()[2]》工具
換句話說,不管是否是 CPython 實現,一個對象的id
就能夠視做是其虛擬的內存地址。this
is
運算 | 含義 |
---|---|
is | object identity |
即
is
的做用是比較對象的標識。spa
——引自《Python 3.7.4 文檔-內置類型[3]》
sys
模塊函數getrefcount()
函數sys.getrefcount(object)
Return the reference count of the object. The count returned is generally one higher than you might expect, because it includes the (temporary) reference as an argument to `getrefcount()`[4].
返回值是傳入對象的引用計數。因爲做爲參數傳入
getrefcount()
的時候產生了一次臨時引用,所以返回的計數值通常要比預期多1。——引自《Python 3.7.4 文檔-sys模塊——系統相關參數及函數[5]》
此處的「引用計數」,在 Python 文檔[6]中被定義爲「對象被引用的次數」。一旦引用計數歸零,則對象所在的內存被釋放。這是 Python 內部進行自動內存管理的一個機制。
C 語言中,變量表明的就是一段固定的內存,而賦給變量的值則是存在這段地址中的數據;但對 Python 來講,變量就再也不是一段固定的地址,而只是 Python 中各個對象所附着的標籤。理解這一點對於理解 Python 的不少特性十分重要。
舉例來講,對於以下的 C 代碼:
int a = 10000;printf("original address: %p\n", &a); // original address: 0060FEFCa = 12345;printf("second address: %p\n", &a); // second address: 0060FEFC
對於有 C 語言編程經驗的人來講,上述結果是顯而易見的:變量a
的地址並不會由於賦給它的值有變化而發生變化。對於 C 編譯器來講,變量a
只是協助它區別各個內存地址的標識,是直接與特定的內存地址綁定的,如圖所示:
>>> a = 10000>>> id(a)1823863879824>>> a = 12345>>> id(a)1823863880176
這就有點兒意思了,更加神奇的是,即便賦給變量同一個常數,其獲得的id
也可能不一樣:
>>> a = 10000>>> id(a)1823863880304>>> a = 10000>>> id(a)1823863879408
假如a
對應的數據類型是一個列表,那麼:
>>> a = [1,2]>>> id(a)2161457994952>>> a = [1,2]>>> id(a)2161458037448
獲得的id
值也是不一樣的。
正如前文所述,在 Python 中,變量就是一塊磚,哪裏須要哪裏搬。每次將一個新的對象賦值給一個變量,都在內存中從新建立了一個對象,這個對象就具備新的引用值。做爲一個「標籤」,變量也是哪裏須要哪裏貼,毫無節操可言。
但要注意的是,這裏還有一個問題:之因此說「即便賦給變量同一個常數,其獲得的
id
也可能不一樣」,其實是由於並非對全部的常數都存在這種狀況。以常數1
爲例,就有以下結果:>>> a = 1>>> id(a)140734357607232>>> a = 1>>> id(a)140734357607232>>> id(1)140734357607232能夠看到,常數
1
對應的id
一直都是相同的,沒有發生變化,所以變量a
的id
也就沒有變化。這是由於Python在內存中維護了一個特定數量的常量池,對於必定範圍內的數值均再也不建立新的對象,而直接在這個常量池中進行分配。實際上在個人機器上使用以下代碼能夠獲得這個常量池的範圍是 [0, 256] ,而 256 恰好是一個字節的二進制碼能夠表示的值的個數。
for b in range(300): if b is not range(300)[b]: print("常量池最大值爲:", (b - 1)) break# 常量池最大值爲:256
相應地,對於數值進行加減乘除並將結果賦給原來的變量,都會改變變量對應的引用值:
>>> a = 10000>>> id(a)2161457772304>>> a = a + 1>>> a10001>>> id(a)2161457772880
比較代碼塊第 三、8行的輸出結果,能夠看到對數值型變量執行加法並賦值會改變對應變量的引用值。這樣的表現應該比較好理解。由於按照 Python 運算符的優先級,a = a + 1
實際上就是a = (a + 1)
,對變量a
對應的數值加1以後獲得的是一個新的數值,再將這個新的數值賦給a
,因而a
的引用也就隨之改變。列表也同樣:
>>> a = [1,2]>>> id(a)2161458326920>>> a = a + [4]>>> a[1, 2, 4]>>> id(a)2161458342792
與數值不一樣,Python 中對列表對象的操做還表現出另外一種特性。考慮下面的代碼:
>>> c = [1, 2, 3]>>> id(c)2161458355400>>> c[2] = 5>>> c[1, 2, 5]>>> id(c)2161458355400>>> c.append(3)>>> c[1, 2, 5, 3]>>> id(c)2161458355400
觀察代碼塊第 三、八、13三行,輸出相同。也就是說,對於列表而言,能夠經過直接操做變量自己,從而在不改變其引用的狀況下改變所引用的值。
更進一步地,若是是兩個變量同時引用同一個列表,則對其中一個變量自己直接進行操做,也會影響到另外一個變量的值:
>>> c = [1, 2, 3]>>> cc = c>>> id(c)1823864610120>>> id(cc)1823864610120
顯然此時的變量c
和cc
的id
是一致的。如今改變c
所引用的列表值:
>>> c[2] = 5>>> cc[1, 2, 5]
能夠看到cc
所引用的列表值也隨之變化了。再看看相應地id
:
>>> id(c)1823864610120>>> id(cc)1823864610120
兩個變量的id
都沒有發生變化。再調用append()
方法:
>>> c.append(3)>>> c[1, 2, 5, 3]>>> cc[1, 2, 5, 3]>>> id(c)1823864610120>>> id(cc)1823864610120
刪除元素:
>>> del c[3]>>> c[1, 2, 5]>>> cc[1, 2, 5]>>> id(c)1823864610120>>> id(cc)1823864610120
在上述全部對列表的操做中,均沒有改變相應元素的引用。
也就是說,對於變量自己進行的操做並不會建立新的對象,而是會直接改變原有對象的值。
本小節示例靈感來自[關於Python中的引用[7]]
數值數據和列表還存在一個特殊的差別。考慮以下代碼:
>>> num = 10000>>> id(num)2161457772336>>> num += 1>>> id(num)2161457774512
有了前面的鋪墊,這樣的結果很顯得很天然。顯然在對變量num
進行增1操做的時候,仍是計算出新值而後進行賦值操做,所以引用發生了變化。
但列表卻否則。見以下代碼:
>>> li = [1, 2, 3]>>> id(li)2161458469960>>> li += [4]>>> id(li)2161458469960>>> li[1, 2, 3, 4]
注意第 4 行。明明進行的是「相加再賦值」操做,爲何有了跟前面不同的結果呢?檢查變量li
的值,發現變量的值也確實發生了改變,但引用卻沒有變。
實際上這是由於加法運算符在 Python 中存在重載的狀況,對列表對象和數值對象來講,加法運算的底層實現是徹底不一樣的,在簡單的加法中,列表的運算仍是建立了一個新的列表對象;但在簡寫的加法運算+=
實現中,則並無建立新的列表對象。這一點要十分注意。
前面(第3天:Python 變量與數據類型[8])咱們提到過,Python 中的六個標準數據類型實際上分爲兩大類:可變數據和不可變數據。其中,列表、字典和集合均爲「可變對象」;而數字、字符串和元組均爲「不可變對象」。實際上上面演示的數值數據(即數字)和列表之間的差別正是這兩種不一樣的數據類型致使的。
因爲數字是不可變對象,咱們不可以對數值自己進行任何能夠改變數據值的操做。所以在 Python 中,每出現一個數值都意味着須要另外分配一個新的內存空間(常量池中的數值例外)。
>>> a = 10000>>> a == 10000True>>> a is 10000False>>> id(a)2161457773424>>> id(10000)2161457773136>>> from sys import getrefcount>>> getrefcount(a)2>>> getrefcount(10000)3
前 9 行的代碼容易理解:即便是一樣的數值,也可能具備不一樣的引用值。關鍵在於這個值是否來自於同一個對象。
而第 10 行的代碼則說明除了getrefcount()
函數的引用外,變量a
所引用的對象就只有1個引用,也就是變量a
。一旦變量a
被釋放,則相應的對象引用計數歸零,也會被釋放;而且只有此時,這個對象對應的內存空間纔是真正的「被釋放」。
而做爲可變對象,列表的值是能夠在不新建對象的狀況下進行改變的,所以對列表對象自己直接進行操做,是能夠達到「改變變量值而不改變引用」的目的的。
對於列表、字典和集合這些「可變對象」,經過對變量所引用對象自己進行操做,能夠只改變變量的值而不改變變量的引用;但對於數字、字符串和元組這些「不可變對象」,因爲對象自己是不可以進行變值操做的,所以要想改變相應變量的值,就必需要新建對象,再把新建對象賦值給變量。
經過這樣的探究,也能更加生動地理解「萬物皆對象」的深入含義。
Python 3.7.4 文檔-內置函數-id()[9]
Python 3.7.4 文檔-內置類型[10]
Python 3.7.4 文檔-sys模塊——系統相關參數及函數[11]
Python 3.7.4 文檔-術語表[12]
關於Python中的引用[13]
[1]id()
: https://docs.python.org/3.7/library/functions.html?highlight=id#id
Python 3.7.4 文檔-內置函數-id(): https://docs.python.org/3.7/library/functions.html?highlight=id#id
[3]Python 3.7.4 文檔-內置類型: https://docs.python.org/3/library/stdtypes.html
[4]getrefcount()
: https://docs.python.org/3.7/library/sys.html#sys.getrefcount
Python 3.7.4 文檔-sys模塊——系統相關參數及函數: https://docs.python.org/3.7/library/sys.html#sys.getrefcount
[6]Python 文檔: https://docs.python.org/3.7/glossary.html?highlight=getrefcount
[7][關於Python中的引用: https://www.cnblogs.com/yuyan/archive/2012/04/21/2461673.html
[8]第3天:Python 變量與數據類型: http://www.ityouknow.com/python/2019/08/03/python-003.html
[9]Python 3.7.4 文檔-內置函數-id(): https://docs.python.org/3.7/library/functions.html?highlight=id#id
[10]Python 3.7.4 文檔-內置類型: https://docs.python.org/3/library/stdtypes.html
[11]Python 3.7.4 文檔-sys模塊——系統相關參數及函數: https://docs.python.org/3.7/library/sys.html#sys.getrefcount
[12]Python 3.7.4 文檔-術語表: https://docs.python.org/3.7/glossary.html?highlight=getrefcount
[13]關於Python中的引用: https://www.cnblogs.com/yuyan/archive/2012/04/21/2461673.html
系列文章 第11天:Python 字典