第17天:Python 之引用


                                                                 

圖片

1. 引用簡介與工具引入

Python 中對於變量的處理與 C 語言有着很大的不一樣,Python 中的變量具備一個特殊的屬性:identity,即「身份標識」。這種特殊的屬性也在不少地方被稱爲「引用」。html

爲了更加清晰地說明引用相關的問題,咱們首先要介紹兩個工具:一個Python的內置函數:id();一個運算符:is;同時還要介紹一個sys模塊內的函數:getrefcount()python

1.1 內置函數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

1.2 運算符is

運算 含義
is object  identity

is的做用是比較對象的標識。spa

——引自《Python 3.7.4 文檔-內置類型[3]

1.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 內部進行自動內存管理的一個機制。

2. 問題示例

C 語言中,變量表明的就是一段固定的內存,而賦給變量的值則是存在這段地址中的數據;但對 Python 來講,變量就再也不是一段固定的地址,而只是 Python 中各個對象所附着的標籤。理解這一點對於理解 Python 的不少特性十分重要。

2.1 對同一變量賦值

舉例來講,對於以下的 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只是協助它區別各個內存地址的標識,是直接與特定的內存地址綁定的,如圖所示:

圖片

但 Python 就不同的。 考慮以下代碼:
>>> 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一直都是相同的,沒有發生變化,所以變量aid也就沒有變化。

這是由於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

2.2 不變的狀況

與數值不一樣,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

顯然此時的變量cccid是一致的。如今改變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

在上述全部對列表的操做中,均沒有改變相應元素的引用。

也就是說,對於變量自己進行的操做並不會建立新的對象,而是會直接改變原有對象的值。

2.3 一個特殊的地方

本小節示例靈感來自[關於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. 原理解析

前面(第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被釋放,則相應的對象引用計數歸零,也會被釋放;而且只有此時,這個對象對應的內存空間纔是真正的「被釋放」。

而做爲可變對象,列表的值是能夠在不新建對象的狀況下進行改變的,所以對列表對象自己直接進行操做,是能夠達到「改變變量值而不改變引用」的目的的。

4. 總結

對於列表、字典和集合這些「可變對象」,經過對變量所引用對象自己進行操做,能夠只改變變量的值而不改變變量的引用;但對於數字、字符串和元組這些「不可變對象」,因爲對象自己是不可以進行變值操做的,所以要想改變相應變量的值,就必需要新建對象,再把新建對象賦值給變量。

經過這樣的探究,也能更加生動地理解「萬物皆對象」的深入含義。

5. 參考資料

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

[2]

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

[5]

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 字典

第10天:Python 類與對象

第9天:Python Tupple

第8天:Python List

第7天:Python 數據結構--序列

第6天:Python 模塊和

第5天:Python 函數

第4天:Python 流程控制

第3天:Python 變量與數據類型

第2天:Python 基礎語法

第1天:Python 環境搭建

相關文章
相關標籤/搜索