by 軒轅禦龍
Python 中對于變量的處理與 C 語言有着很大的不同,Python 中的變量具有一個特殊的屬性:identity,即“身份辨別”。這種特殊的屬性也在很多地方被稱為“引用”。
為了更加清晰地說明引用相關的問題,我們首先要介紹兩個工具:一個Python的内置函數:<code>id()</code>;一個運算符:<code>is</code>;同時還要介紹一個<code>sys</code>子產品内的函數:<code>getrefcount()</code>。
<code>id(object)</code>
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 <code>id()</code> value. 傳回值為傳入對象的“辨別”。該辨別是一個唯一的常數,在傳入對象的生命周期内與之一一對應。生命周期沒有重合的兩個對象可能擁有相同的<code>id()</code>傳回值。
CPython implementation detail: This is the address of the object in memory. CPython 實作細節:“辨別”實際上就是對象在記憶體中的位址。
——引自《Python 3.7.4 文檔-内置函數-id()》
換句話說,不論是否是 CPython 實作,一個對象的<code>id</code>就可以視作是其虛拟的記憶體位址。
運算
含義
is
object identity
即<code>is</code>的作用是比較對象的辨別。
——引自《Python 3.7.4 文檔-内置類型》
<code>sys.getrefcount(object)</code>
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 <code>getrefcount()</code>. 傳回值是傳入對象的引用計數。由于作為參數傳入<code>getrefcount()</code>的時候産生了一次臨時引用,是以傳回的計數值一般要比預期多1。 ——引自《Python 3.7.4 文檔-sys子產品——系統相關參數及函數》
此處的“引用計數”,在 Python 文檔中被定義為“對象被引用的次數”。一旦引用計數歸零,則對象所在的記憶體被釋放。這是 Python 内部進行自動記憶體管理的一個機制。
C 語言中,變量代表的就是一段固定的記憶體,而賦給變量的值則是存在這段位址中的資料;但對 Python 來說,變量就不再是一段固定的位址,而隻是 Python 中各個對象所附着的标簽。了解這一點對于了解 Python 的很多特性十分重要。
舉例來說,對于如下的 C 代碼:
對于有 C 語言程式設計經驗的人來說,上述結果是顯而易見的:變量<code>c_variable</code>的位址并不會因為賦給它的值有變化而發生變化。對于 C 編譯器來說,變量<code>c_variable</code>隻是協助它差別各個記憶體位址的辨別,是直接與特定的記憶體位址綁定的,如圖所示:

但 Python 就不一樣的。考慮如下代碼:
這就有點兒意思了,更加神奇的是,即使賦給變量同一個常數,其得到的<code>id</code>也可能不同:
假如<code>python_variable</code>對應的資料類型是一個清單,那麼:
得到的<code>id</code>值也是不同的。
正如前文所述,在 Python 中,變量就是一塊磚,哪裡需要哪裡搬。每次将一個新的對象指派給一個變量,都在記憶體中重新建立了一個對象,這個對象就具有新的引用值。作為一個“标簽”,變量也是哪裡需要哪裡貼,毫無節操可言。
但要注意的是,這裡還有一個問題:之是以說“即使賦給變量同一個常數,其得到的<code>id</code>也可能不同”,實際上是因為并不是對所有的常數都存在這種情況。以常數<code>1</code>為例,就有如下結果:
可以看到,常數<code>1</code>對應的<code>id</code>一直都是相同的,沒有發生變化,是以變量<code>littleConst</code>的<code>id</code>也就沒有變化。 這是因為Python在記憶體中維護了一個特定數量的常量池,對于一定範圍内的數值均不再建立新的對象,而直接在這個常量池中進行配置設定。實際上在我的機器上使用如下代碼可以得到這個常量池的範圍是 [0, 256] ,而 256 剛好是一個位元組的二進制碼可以表示的值的個數。
相應地,對于數值進行加減乘除并将結果賦給原來的變量,都會改變變量對應的引用值:
比較代碼塊第 3、8行的輸出結果,可以看到對數值型變量執行加法并指派會改變對應變量的引用值。這樣的表現應該比較好了解。因為按照 Python 運算符的優先級,<code>change_ref = change_ref + 1</code>實際上就是<code>change_ref = (change_ref + 1)</code>,對變量<code>change_ref</code>對應的數值加1之後得到的是一個新的數值,再将這個新的數值賦給<code>change_ref</code> ,于是<code>change_ref</code>的引用也就随之改變。清單也一樣:
與數值不同,Python 中對清單對象的操作還表現出另一種特性。考慮下面的代碼:
觀察代碼塊第 3、8、13三行,輸出相同。也就是說,對于清單而言,可以通過直接操作變量本身,進而在不改變其引用的情況下改變所引用的值。
更進一步地,如果是兩個變量同時引用同一個清單,則對其中一個變量本身直接進行操作,也會影響到另一個變量的值:
顯然此時的變量<code>list_example</code>和<code>list_same_ref</code>的<code>id</code>是一緻的。現在改變<code>list_example</code>所引用的清單值:
可以看到<code>list_same_ref</code>所引用的清單值也随之變化了。再看看相應地<code>id</code>:
兩個變量的<code>id</code>都沒有發生變化。再調用<code>append()</code>方法:
删除元素:
在上述所有對清單的操作中,均沒有改變相應元素的引用。
也就是說,對于變量本身進行的操作并不會建立新的對象,而是會直接改變原有對象的值。
本小節示例靈感來自[關于Python中的引用]
數值資料和清單還存在一個特殊的差異。考慮如下代碼:
有了前面的鋪墊,這樣的結果很顯得很自然。顯然在對變量<code>num</code>進行增1操作的時候,還是計算出新值然後進行指派操作,是以引用發生了變化。
但清單卻不然。見如下代碼:
注意第 4 行。明明進行的是“相加再指派”操作,為什麼有了跟前面不一樣的結果呢?檢查變量<code>li</code>的值,發現變量的值也确實發生了改變,但引用卻沒有變。
實際上這是因為加法運算符在 Python 中存在重載的情況,對清單對象和數值對象來說,加法運算的底層實作是完全不同的,在簡單的加法中,清單的運算還是建立了一個新的清單對象;但在簡寫的加法運算<code>+=</code>實作中,則并沒有建立新的清單對象。這一點要十分注意。
前面(第3天:Python 變量與資料類型)我們提到過,Python 中的六個标準資料類型實際上分為兩大類:可變資料和不可變資料。其中,清單、字典和集合均為“可變對象”;而數字、字元串和元組均為“不可變對象”。實際上上面示範的數值資料(即數字)和清單之間的差異正是這兩種不同的資料類型導緻的。
由于數字是不可變對象,我們不能夠對數值本身進行任何可以改變資料值的操作。是以在 Python 中,每出現一個數值都意味着需要另外配置設定一個新的記憶體空間(常量池中的數值例外)。
前 9 行的代碼容易了解:即使是同樣的數值,也可能具有不同的引用值。關鍵在于這個值是否來自于同一個對象。
而第 12 行的代碼則說明除了<code>getrefcount()</code>函數的引用外,變量<code>const_ref</code>所引用的對象就隻有1個引用,也就是變量<code>const_ref</code>。一旦變量<code>const_ref</code>被釋放,則相應的對象引用計數歸零,也會被釋放;并且隻有此時,這個對象對應的記憶體空間才是真正的“被釋放”。
而作為可變對象,清單的值是可以在不建立對象的情況下進行改變的,是以對清單對象本身直接進行操作,是可以達到“改變變量值而不改變引用”的目的的。
對于清單、字典和集合這些“可變對象”,通過對變量所引用對象本身進行操作,可以隻改變變量的值而不改變變量的引用;但對于數字、字元串和元組這些“不可變對象”,由于對象本身是不能夠進行變值操作的,是以要想改變相應變量的值,就必須要建立對象,再把建立對象指派給變量。
通過這樣的探究,也能更加生動地了解“萬物皆對象”的深刻含義。0
示例代碼:Python-100-days-day012
Python 3.7.4 文檔-内置函數-id()
Python 3.7.4 文檔-内置類型
Python 3.7.4 文檔-sys子產品——系統相關參數及函數
Python 3.7.4 文檔-術語表
關于Python中的引用
關注公衆号:python技術,回複"python"一起學習交流
作者:純潔的微笑
出處:www.ityouknow.com
資源:微信搜【純潔的微笑】關注我,回複 【程式員】【面試】【架構師】有我準備的一線程式必備計算機書籍、大廠面試資料和免費電子書。 一共1024G的資料,希望可以幫助大家提升技術和能力。
本文如對您有幫助,還請多幫 【推薦】 下此文。
點我了解:Tooool-程式員一站式導航網站