標籤:python small int caching
1. Python的物件模型
我們知道,在Python的世界裡,萬物皆對象(Object)。根據Python官方文檔對Data Model的說明,每個Python對象均擁有3個特性:身份、類型和值。
官方文檔關於物件模型的這段概括說明對於我們理解Python對象是如此重要,所以本文將其摘錄如下(為了使得結構更清晰,這裡把原文檔做了分段處理):
1) Every object has an identity, a type and a value.
2) An object‘s identity never changes once it has been created; you may think of it as the object‘s address in memory. The ‘is‘ operator compares the identity of two objects; the id() function returns an integer representing its identity (currently implemented as its address).
3) An object‘s type is also unchangeable. An object‘s type determines the operations that the object supports (e.g., "does it have a length?") and also defines the possible values for objects of that type. The type() function returns an object‘s type (which is an object itself).
4) The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter‘s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.)
5) An object‘s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.
總結一下:
1) 每個Python對象均有3個特性:身份、類型和值
2) 對象一旦建立,其身份(可以理解為對象的記憶體位址)就是不可變的。可以藉助Python的built-in函數id()來擷取對象的id值,可以藉助is操作符來比較兩個對象是否是同一個對象
3) 已建立對象的類型不可更改,對象的類型決定了可以作用於該對象上的操作,也決定了該對象可能支援的值
4) 某些對象(如list/dict)的value可以修改,這類對象被稱為mutable object;而另一些對象(如numbers/strings/tuples)一旦建立,其value就不可修改,故被稱為immutable object
5) 對象的值是否可以被修改是由其type決定的
2. 執行個體說明immutable object的值的“修改”行為
由上面的描述可知,數字類型的對象是不可變對象。為加深理解,考慮下面的範例程式碼。
>>> x = 2.11>>> id(x) 7223328>>> x += 0.5>>> x2.61>>> id(x)7223376
上述代碼中,x += 0.5看起來像是修改了名為x的對象的值。
但事實上,在Python底層實現中,x只是個指標,它指向對象的引用,也即x並不是一個數字類型的對象。
上述代碼真正發生的事情是:
1) 值為2.11的float類型對象被建立,其引用計數值為1
2) x作為引用指向了剛才建立的對象,對象的引用計數值變為2
3) 當執行"x += 0.5"時,值為2.61的float類型對象被建立(其初始引用計數值為1),x作為引用指向了這個新對象(這意味著新對象的引用計數值變為2,而第1個對象的引用計數值由於x的"解引用"而減為1)
可見,上述代碼並沒有修改名為x的對象的值,標識符x只是通過重新引用指向了新建立的對象,讓我們誤以為其值被“修改”了而已。
3. 一個“古怪”的case
按照上述說明,下面的case如何理解呢?
>>> a = 20>>> b = 20>>> id(a)7151888>>> id(b)7151888>>> a is bTrue
上述代碼中,
a和b應該是不同的對象的引用,它們的id值不相等才對。
但id(a) == id(b)及"a is b"輸出"True"的事實表明,CPython解譯器顯然不是按照我們的預期來執行的。
難道是解譯器實現有bug嗎?
4. 從CPython實現PyIntObject的源碼來揭秘
事實上,上面看到的不符合預期的古怪case與CPython實現PyIntObject類型時所作的最佳化有關。
《Python核心編程》一書第4.5.2節提到:
整數對象是不可變對象,所以Python會高效的緩衝它,而這會造成我們認為Python應該建立新對象時,它卻沒有建立新對象的假象。
這正是我們剛才遇到的“古怪”case的底層原因。
為了證實這一點,我查看了CPython v2.7開源在github上的源碼(cpython/Objects/intobject.c),可以看到下面一段代碼:
#ifndef NSMALLPOSINTS#define NSMALLPOSINTS 257#endif#ifndef NSMALLNEGINTS#define NSMALLNEGINTS 5#endif#if NSMALLNEGINTS + NSMALLPOSINTS > 0/* References to small integers are saved in this array so that they can be shared. The integers that are saved are those in the range -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).*/static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];#endif
可見,
解譯器實現int型對象時,確實申請了一個small_ints數組用於緩衝小整數,從宏定義及注釋可以看到,緩衝的整數範圍是[-5, 257)。
在該源碼檔案中搜尋"small_ints"還可以看到,該數組被4個函數用到,函數名分別為:_PyInt_Init, PyInt_FromLong, PyInt_ClearFreeList, PyInt_Fini
其中,後兩個函數與資源釋放相關,我們此處不關心;而在_PyInt_Init中,構造一系列small int對象並存入small_ints數組;在PyObject * PyInt_FromLong(long ival)函數中,若構造的是個small int(即傳入的ival在small int範圍內),則直接返回small_ints數組中的緩衝對象,若傳入的ival不在small int訪問內,則建立新對象並返回其引用。
至此,我們大概清楚了CPython解譯器實現int型對象的細節行為,也知道了我們遇到的那個古怪case的原因。
在互動模式下,我們已經看到CPython解譯器確實會緩衝小整數對象,事實上,CPython在編譯py指令碼時(編譯成bytecodes),還會做其它與文檔說明不符的最佳化,StackOverflow上的這篇文章Weird Integer Cache inside Python 2.6對此做了詳細說明,值得研讀。
總之,解譯器的實現細節我們無法幹預,但是,在編寫應用程式時,我們要確保函數邏輯不會依賴“解譯器會緩衝小整數”的這個特性,以免踩到詭異的坑。
【參考資料】
1. Python Doc: Data model
2. Section 4.5.2 of <Core Python Programming>,即《Python核心編程》第4.5.2節
3. Python Doc: Plain Integer Objects - PyInt_FromLong(long ival)
4. GitHub Repo - CPython Source Code: CPython/2.7/Objects/intobject.c
5. StackOverflow: Weird Integer Cache inside Python 2.6
===================== EOF =======================
【Python筆記】從一個“古怪”的case探究CPython對Int對象的實現細節