注:本文不區分作為編程語言的Python和作為語言實現的Python。后者均默認為CPython。
了解他人對Python源代碼的掌握情況,我喜歡問這樣一個問題
請問,在Python中,256和257的主要區別是什么?
我期望的回答是
Python內部,對這兩個數采取了不同的對象創建策略
1.做一個實驗
我們知道,在一個對象的生存期內,可以用id()函數得到這個對象的唯一標識。即,id返回值相同的對象一定是同一個對象。
啟動Python的交互模式(主流版本的Python 2和Python 3均可),輸入以下語句並觀察結果。
>>> a = 0
>>> b = 0
>>> id(a) == id(b)
True
>>> a = -5
>>> b = -5
>>> id(a) == id(b)
True
>>> a = -6
>>> b = -6
>>> id(a) == id(b)
False
>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 257
>>> b = 257
>>> id(a) == id(b)
False
>>> a = -1.0
>>> b = -1.0
>>> id(a) == id(b)
False
你也可以寫一個帶有for循環的腳本,更加全面的驗證這樣一個結論:
Python中,對於整數對象,如果其值處於[-5,256]的閉區間內,則值相同的對象是同一個對象
您有可能想到了,這也許和Python內部的某種機制有關。讓我們更加深入的使用API來驗證這個結論。
2. 使用Python API
以Python 2為例,可以使用這樣的代碼得到與Python腳本等價的結論:
#include <Python.h>
int main(int argc,char ** argv) {
PyObject *a,*b;
Py_SetProgramName(argv[0]);
Py_Initialize();
a = PyInt_FromLong(256);
b = PyInt_FromLong(256);
printf("a=%p,b=%p\n",a,b); //value should be the same
a = PyInt_FromLong(257);
b = PyInt_FromLong(257);
printf("a=%p,b=%p\n",a,b); //value should be different
Py_Finalize();
return 0;
}
如果使用Python3 ,PyInt_FromLong要替換為PyLong_FromLong。
從運行結果可以看到,從256產生的兩個PyObject*,指向了內存中相同的地址,但是從257產生的PyObject則是相互獨立的。
3.沒有什么好奇怪的
為什么是-5到256之間的這200多個數?其實沒有什么奇怪的,Python本身就是這樣實現的。
讓我們打開源代碼一看究竟。首先看看Python2的實現方式。下面的代碼是以本文寫作時最新的2.7.14為例子的。
在Python自身的main函數里,會調用Py_Initialize這個函數初始化Python內部的一系列模塊。(Modules/main.c,551行)。在初始化過程中,_PyInt_Init會被調用(Python/pythonrun.c,210行)。_PyInt_Init的唯一作用就是初始化small_ints數組(Objects/intobject.c,1452行):
int
_PyInt_Init(void)
{
PyIntObject *v;
int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
if (!free_list && (free_list = fill_free_list()) == NULL)
return 0;
/* PyObject_New is inlined */
v = free_list;
free_list = (PyIntObject *)Py_TYPE(v);
(void)PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
return 1;
}
我們看到了兩個宏:NSMALLNEGINTS 和NSMALLPOSINTS 。在intobject.c的頭部找到它們的定義:
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
從_PyInt_Init的實現上,我們可以看到被放入small_ints的數字范圍是-5到256。因此,你可以通過修改源代碼的方式,將這個范圍任意的擴展。
如果你不對這兩個宏進行改動,那么在Python啟動的時候,會先創建一個200多PyObject大小的數組,默認的把-5從256的所有整數創建完畢。
我們知道,Python在遇到諸如 a = 5這樣的語句的時候,最終會落到PyInt_FromLong這個函數里。我們看看這個函數是怎么寫的(intobject.c,86行):
PyObject *
PyInt_FromLong(long ival)
{
register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
v = small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
#ifdef COUNT_ALLOCS
if (ival >= 0)
quick_int_allocs++;
else
quick_neg_int_allocs++;
#endif
return (PyObject *) v;
}
#endif
if (free_list == NULL) {
if ((free_list = fill_free_list()) == NULL)
return NULL;
}
/* Inline PyObject_New */
v = free_list;
free_list = (PyIntObject *)Py_TYPE(v);
(void)PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}
這段代碼很容易讀懂,首先判斷字面量的范圍是不是在-5到256之間,如果是,直接從small_ints里面取得緩存的對象,如果不是,再通過PyObject_New來創建一個新的對象。
Python3 的代碼以此類推,相似的代碼在longobject.c里面。
4. 為什么要這樣做
主要還是性能上的考慮。由於創建一個新的對象是比較折騰的:在內存池中分配空間,賦予對象的類別並賦予其初始的值。從-5到256這些小的整數,在Python腳本中使用的非常頻繁,又因為他們是不可更改的,因此只創建一次,重復使用就可以了。
有興趣的讀者可以把負責緩存邊界的兩個宏改小,或者讓它們的和是負數以取消這個功能,看看日常的腳本是否有性能的變化。
5. 一點擴展……
考慮這樣的命令:
>>> a,b = 400,400
>>> id(a) == id(b)
True
您知道是為什么嗎?
以上內容轉載自 知乎