第12天:Python 之引用


by 軒轅御龍

Python 之引用

1. 引用簡介與工具引入

Python 中對於變量的處理與 C 語言有着很大的不同,Python 中的變量具有一個特殊的屬性:identity,即“身份標識”。這種特殊的屬性也在很多地方被稱為“引用”。

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

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() value.

返回值為傳入對象的“標識”。該標識是一個唯一的常數,在傳入對象的生命周期內與之一一對應。生命周期沒有重合的兩個對象可能擁有相同的id()返回值。

CPython implementation detail: This is the address of the object in memory.

CPython 實現細節:“標識”實際上就是對象在內存中的地址。

——引自《Python 3.7.4 文檔-內置函數-id()

換句話說,不論是否是 CPython 實現,一個對象的id就可以視作是其虛擬的內存地址。

1.2 運算符is

運算 含義
is object identity

is的作用是比較對象的標識。

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

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().

返回值是傳入對象的引用計數。由於作為參數傳入getrefcount()的時候產生了一次臨時引用,因此返回的計數值一般要比預期多1。

——引自《Python 3.7.4 文檔-sys模塊——系統相關參數及函數

此處的“引用計數”,在 Python 文檔中被定義為“對象被引用的次數”。一旦引用計數歸零,則對象所在的內存被釋放。這是 Python 內部進行自動內存管理的一個機制。

2. 問題示例

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

2.1 對同一變量賦值

舉例來說,對於如下的 C 代碼:

int c_variable = 10000;
printf("original address: %p\n", &a); // original address: 0060FEFC
c_variable = 12345;
printf("second address: %p\n", &a); // second address: 0060FEFC

對於有 C 語言編程經驗的人來說,上述結果是顯而易見的:變量c_variable的地址並不會因為賦給它的值有變化而發生變化。對於 C 編譯器來說,變量c_variable只是協助它區別各個內存地址的標識,是直接與特定的內存地址綁定的,如圖所示:

C 語言中的變量賦值

但 Python 就不一樣的。考慮如下代碼:

>>> python_variable = 10000
>>> id(python_variable)
1823863879824
>>> python_variable = 12345
>>> id(python_variable)
1823863880176

這就有點兒意思了,更加神奇的是,即使賦給變量同一個常數,其得到的id也可能不同:

>>> python_variable = 10000
>>> id(python_variable)
1823863880304
>>> python_variable = 10000
>>> id(python_variable)
1823863879408

假如python_variable對應的數據類型是一個列表,那么:

>>> python_variable = [1,2]
>>> id(python_variable)
2161457994952
>>> python_variable = [1,2]
>>> id(python_variable)
2161458037448

得到的id值也是不同的。

正如前文所述,在 Python 中,變量就是一塊磚,哪里需要哪里搬。每次將一個新的對象賦值給一個變量,都在內存中重新創建了一個對象,這個對象就具有新的引用值。作為一個“標簽”,變量也是哪里需要哪里貼,毫無節操可言。

Python 中的變量賦值

但要注意的是,這里還有一個問題:之所以說“即使賦給變量同一個常數,其得到的id可能不同”,實際上是因為並不是對所有的常數都存在這種情況。以常數1為例,就有如下結果:

>>> littleConst = 1 # 數值較小的整型對象
>>> id(littleConst)
140734357607232
>>> littleConst = 1
>>> id(littleConst)
140734357607232
>>> id(1)
140734357607232

可以看到,常數1對應的id一直都是相同的,沒有發生變化,因此變量littleConstid也就沒有變化。

這是因為Python在內存中維護了一個特定數量的常量池,對於一定范圍內的數值均不再創建新的對象,而直接在這個常量池中進行分配。實際上在我的機器上使用如下代碼可以得到這個常量池的范圍是 [0, 256] ,而 256 剛好是一個字節的二進制碼可以表示的值的個數。

for constant in range(300):
    if constant is not range(300)[constant]:
        print("常量池最大值為:", (constant - 1))
        break
# 常量池最大值為: 256

相應地,對於數值進行加減乘除並將結果賦給原來的變量,都會改變變量對應的引用值:

>>> change_ref = 10000
>>> id(change_ref)
2161457772304
>>> change_ref = change_ref + 1
>>> change_ref
10001
>>> id(change_ref)
2161457772880

比較代碼塊第 3、8行的輸出結果,可以看到對數值型變量執行加法並賦值會改變對應變量的引用值。這樣的表現應該比較好理解。因為按照 Python 運算符的優先級,change_ref = change_ref + 1實際上就是change_ref = (change_ref + 1),對變量change_ref對應的數值加1之后得到的是一個新的數值,再將這個新的數值賦給change_ref ,於是change_ref的引用也就隨之改變。列表也一樣:

>>> list_change_ref = [1,2]
>>> id(list_change_ref)
2161458326920
>>> list_change_ref = list_change_ref + [4]
>>> list_change_ref
[1, 2, 4]
>>> id(list_change_ref)
2161458342792

2.2 不變的情況

與數值不同,Python 中對列表對象的操作還表現出另一種特性。考慮下面的代碼:

>>> list_nonchange = [1, 2, 3]
>>> id(list_nonchange)
2161458355400
>>> list_nonchange[2] = 5
>>> list_nonchange
[1, 2, 5]
>>> id(list_nonchange)
2161458355400
>>> list_nonchange.append(3)
>>> list_nonchange
[1, 2, 5, 3]
>>> id(list_nonchange)
2161458355400

觀察代碼塊第 3、8、13三行,輸出相同。也就是說,對於列表而言,可以通過直接操作變量本身,從而在不改變其引用的情況下改變所引用的值。

更進一步地,如果是兩個變量同時引用同一個列表,則對其中一個變量本身直接進行操作,也會影響到另一個變量的值:

>>> list_example = [1, 2, 3]
>>> list_same_ref = list_example
>>> id(list_example)
1823864610120
>>> id(list_same_ref)
1823864610120

顯然此時的變量list_examplelist_same_refid是一致的。現在改變list_example所引用的列表值:

>>> list_example[2] = 5
>>> list_same_ref
[1, 2, 5]

可以看到list_same_ref所引用的列表值也隨之變化了。再看看相應地id

>>> id(list_example)
1823864610120
>>> id(list_same_ref)
1823864610120

兩個變量的id都沒有發生變化。再調用append()方法:

>>> list_example.append(3)
>>> list_example
[1, 2, 5, 3]
>>> list_same_ref
[1, 2, 5, 3]
>>> id(list_example)
1823864610120
>>> id(list_same_ref)
1823864610120

刪除元素:

>>> del list_example[3]
>>> list_example
[1, 2, 5]
>>> list_same_ref
[1, 2, 5]
>>> id(list_example)
1823864610120
>>> id(list_same_ref)
1823864610120

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

也就是說,對於變量本身進行的操作並不會創建新的對象,而是會直接改變原有對象的值。

2.3 一個特殊的地方

本小節示例靈感來自[關於Python中的引用]

數值數據和列表還存在一個特殊的差異。考慮如下代碼:

>>> 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 變量與數據類型)我們提到過,Python 中的六個標准數據類型實際上分為兩大類:可變數據不可變數據。其中,列表、字典和集合均為“可變對象”;而數字、字符串和元組均為“不可變對象”。實際上上面演示的數值數據(即數字)和列表之間的差異正是這兩種不同的數據類型導致的。

由於數字是不可變對象,我們不能夠對數值本身進行任何可以改變數據值的操作。因此在 Python 中,每出現一個數值都意味着需要另外分配一個新的內存空間(常量池中的數值例外)。

>>> const_ref = 10000 # 
>>> const_ref == 10000
True
>>> const_ref is 10000
False
>>> id(const_ref)
2161457773424
>>> id(10000)
2161457773136
>>> from sys import getrefcount
>>> getrefcount(const_ref)
2
>>> getrefcount(10000)
3

前 9 行的代碼容易理解:即使是同樣的數值,也可能具有不同的引用值。關鍵在於這個值是否來自於同一個對象。

而第 12 行的代碼則說明除了getrefcount()函數的引用外,變量const_ref所引用的對象就只有1個引用,也就是變量const_ref。一旦變量const_ref被釋放,則相應的對象引用計數歸零,也會被釋放;並且只有此時,這個對象對應的內存空間才是真正的“被釋放”。

而作為可變對象,列表的值是可以在不新建對象的情況下進行改變的,因此對列表對象本身直接進行操作,是可以達到“改變變量值而不改變引用”的目的的。

4. 總結

對於列表、字典和集合這些“可變對象”,通過對變量所引用對象本身進行操作,可以只改變變量的值而不改變變量的引用;但對於數字、字符串和元組這些“不可變對象”,由於對象本身是不能夠進行變值操作的,因此要想改變相應變量的值,就必須要新建對象,再把新建對象賦值給變量。

通過這樣的探究,也能更加生動地理解“萬物皆對象”的深刻含義。0

示例代碼:Python-100-days-day012

5. 參考資料

Python 3.7.4 文檔-內置函數-id()

Python 3.7.4 文檔-內置類型

Python 3.7.4 文檔-sys模塊——系統相關參數及函數

Python 3.7.4 文檔-術語表

關於Python中的引用

關注公眾號:python技術,回復"python"一起學習交流


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM