Python中神秘的-5到256


注:本文不區分作為編程語言的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

您知道是為什么嗎?


以上內容轉載自 知乎


免責聲明!

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



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