1. 程式人生 > 實用技巧 >4. 解密Python中最簡單的物件--浮點數的底層實現

4. 解密Python中最簡單的物件--浮點數的底層實現

楔子

從現在開始,我們就來分析Python中常見的內建物件、以及對應的例項物件,看看它們在底層是如何實現的。但說實話,我們在前面幾節中介紹物件的時候,已經說了不少了,不過從現在開始要進行更深入的分析。

除了物件本身,還要看物件支援的操作在底層是如何實現的。我們首先以浮點數為例,因為它是最簡單的,沒錯,浮點數比整型要簡單。至於為什麼,當我們分析整型的時候就知道了。

內部物件

float例項物件定義在Include/floatobject.h中,結構非常簡單:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

除了PyObject這個公共的頭部資訊之外,只有一個額外的ob_fval,用於儲存具體的值,而且直接使用的C中的double。

那麼float型別物件在底層長啥樣子呢?

與例項物件不同,float型別物件全域性唯一,因此可以作為全域性變數定義。底層對應PyFloat_Type,位於Objects/typeobject.c中。

PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),
    0,
    (destructor)float_dealloc,                  /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    (reprfunc)float_repr,                       /* tp_repr */
    &float_as_number,                           /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    (hashfunc)float_hash,                       /* tp_hash */
    0,                                          /* tp_call */
    (reprfunc)float_repr,                       /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    float_new__doc__,                           /* tp_doc */
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    float_richcompare,                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    float_methods,                              /* tp_methods */
    0,                                          /* tp_members */
    float_getset,                               /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    float_new,                                  /* tp_new */
};

PyFloat_Type中儲存了很多關於浮點數物件的元資訊,關鍵欄位包括:

  • tp_name欄位儲存了型別名稱,是一個char *,顯然是"float";
  • tp_dealloc、tp_init、tp_alloc和 tp_new欄位是與物件建立銷燬相關的函式;
  • tp_repr欄位對應__repr__方法,生成語法字串;
  • tp_str欄位對應__str__方法,生成普通字串;
  • tp_as_number欄位對應數值物件支援的操作簇;
  • tp_hash欄位是雜湊值生成函式;

PyFloat_Type很重要,作為浮點型別物件,它決定了浮點數的生死和行為。

物件的建立

在上一篇部落格中,我們初步瞭解到建立例項物件的一般過程。對於內建型別的例項物件,可以使用Python/C API建立,也可以通過呼叫型別物件建立。

呼叫型別物件float建立例項物件,Python執行的是type型別物件中的tp_call函式。tp_call中會先呼叫型別物件的tp_new為該物件的例項物件申請一份空間,申請完畢之後該物件就已經被建立了。然後會再呼叫tp_init,並將例項物件作為引數傳遞進去,進行初始化,也就是設定屬性。

但是對於float來說,它內部的tp_init成員是0,從PyFloat_Type的定義我們也可以看到。說明float沒有__init__函式,原因是float是一種很簡單的型別物件,初始化操作只需要一個賦值語句,所以在tp_new中就可以完成。

除了通過呼叫型別物件建立例項物件這種通用型方法之外,CPython還為內建型別物件提供了一些Python/C API來建立對應的例項物件。可以簡化呼叫,提高效率。關於為什麼可以提高效率,我們之前已經分析過了,我們說通過Python/C API建立的話,會直接解析成底層對應的資料結構,而通過型別物件呼叫的話則會有一些額外的開銷。

PyObject *
PyFloat_FromDouble(double fval);

PyObject *
PyFloat_FromString(PyObject *v);

以上是底層提供的兩個建立浮點數的C API,當然還有其它的。

  • PyFloat_FromDouble:通過C中的double建立float物件;
  • PyFloat_FromString:通過字串物件建立float物件;

以PyFloat_FromDouble為例,我們看看底層是怎麼建立的?該函式同樣位於Objects/floatobject.c中。

PyObject *
PyFloat_FromDouble(double fval)
{	
    //我們之前在介紹引用計數的時候,說過引用計數為0了,那麼物件會被銷燬
    //但是物件所佔的記憶體則不一定回收、或者說還給作業系統,而是會快取起來
    //所以從這行程式碼我們就看到了,建立浮點數物件的時候會優先從快取池裡面獲取
    //而快取池是使用連結串列實現的,free_list(指標)指向的連結串列的第一個物件
    PyFloatObject *op = free_list;
    //op不是NULL,說明快取池中有物件,成功獲取
    if (op != NULL) {
        //一旦獲取了,那麼要將free_list指向連結串列中當前獲取的物件的下一個物件
        //但是Py_TYPE不是一個巨集嗎?它獲取的應該是物件的ob_type啊,那麼Py_TYPE(op)獲取的不是PyFloat_Type指標嗎?別急這一點我們後面會說
        free_list = (PyFloatObject *) Py_TYPE(op); 
        //並且將快取池的內部可以使用的浮點數物件的數量減1
        //關於快取池, 以及為什麼要使用快取池下面也會細說
        //目前先知道Python在分配浮點數物件的時候會先從快取池裡面獲取就可以了
        numfree--;
    } else {
        //否則的話,呼叫PyObject_MALLOC申請記憶體,PyObject_MALLOC是基於malloc的一個封裝
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        //申請失敗的話,證明記憶體不夠了
        if (!op)
            return PyErr_NoMemory();
    }
	
    //走到這裡說明記憶體分配好了,PyFloatObject也建立了,但是不是還少了點啥呢?顯然內部的成員還沒有初始化
    //還是那句話內建型別的例項物件該分配多少空間,直譯器瞭如指掌,因為通過PyFloatObject內部的成員一算就出來了。
    //因此雖然物件建立了,但是此時內部的ob_refcnt、ob_type、以及ob_fval三個成員還沒有被初始化。
    //所以還要將其ob_refcnt設定為1(因為對於剛建立的物件來說,內部的引用計數顯然為1),將ob_type設定為指向PyFloat_Type的指標
    //而PyObject_INIT是一個巨集,它就是專門用來設定ob_type以及ob_refcnt的,我們後面看這個巨集的定義就知道了
    (void)PyObject_INIT(op, &PyFloat_Type);
    //將內部的ob_fval成員設定為fval,所以此時三個成員都已經初始化完畢
    op->ob_fval = fval;
    //將其轉成PyObject *返回
    return (PyObject *) op;
}

所以整體流程如下:

  • 1. 為例項物件分配記憶體空間,空間分配完了物件也就建立了,不過會優先使用快取池;
  • 2. 初始化例項物件內部的引用計數和型別指標;
  • 3. 初始化ob_fval為指定的浮點值;

然後我們看一下PyObject_INIT這個巨集,它位於Include/objimpl.h中。

#define PyObject_INIT(op, typeobj) \
    ( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
//這個巨集接收兩個引數,分別是:例項物件的指標和指向的型別物件的指標
//然後Py_TYPE(op)表示獲取其內部的ob_type, 將其設定為typeobj, 而typeobj在原始碼中傳入的就是&PyFloat_Type
//然後是_Py_NewReference, 這個巨集我們在上一篇部落格中已經說過了,它用於將物件的引用計數初始化為1

物件的銷燬

當刪除一個變數時,Python會通過巨集Py_DECREF或者Py_XDECREF來減少該變數指向的物件的引用計數;當引用計數為0時,就會回收該物件。而回收該物件會呼叫其型別物件中的tp_dealloc指向的函式。當然啦,CPython依舊為回收物件提供了一個巨集,我們上一篇中也說過了。

#define _Py_Dealloc(op) (                               \
    _Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA          \
    (*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))
// _Py_Dealloc(op)會呼叫op指向的物件的型別物件中的解構函式,同時將op自身作為引數傳遞進去,表示將op指向的物件回收。

而PyFloat_Type中的tp_dealloc成員被初始化為float_dealloc,所以解構函式最終執行的是float_dealloc,關於它的原始碼我們會在一會兒介紹快取池的時候細說。

總結一下的話,浮點數物件從建立到銷燬整個生命週期所涉及的關鍵函式、巨集、呼叫關係可以如下圖所示:

我們看到通過型別物件呼叫的方式來建立例項物件,最終也是要走Python/C API的,肯定沒有直接通過Python/C API建立的方式快,因為前者多了幾個步驟。

所以如果是float(3.14),那麼最終也會呼叫PyFloat_FromDouble(3.14);如果是float("3.14"),那麼最終會呼叫PyFloat_FromString("3.14")。所以呼叫型別物件的時候,會先兜個圈子再去使用Python/C API,肯定沒有直接使用Python/C API的效率高。

快取池

我們說浮點數這種物件是經常容易被建立和銷燬的,如果每建立一個就分配一次記憶體、每銷燬一個就回收一次記憶體的話,那效率會低到可想而知了。我們知道Python在作業系統之上封裝了一個記憶體池,可以用於小記憶體物件的快速建立和銷燬,這便是Python的記憶體池機制。但浮點數使用的頻率很高,我們有時會建立和銷燬大量的臨時物件,所以如果每一次物件的建立和銷燬都伴隨著記憶體相關的操作的話,這個時候即便是有記憶體池機制,效率也是不高的。

考慮如下程式碼:

>>> pi = 3.14
>>> r = 2.0
>>> s = pi * r ** 2
>>> s
12.56
>>>

這個語句首先計算半徑r的平方,然後根據結果建立一個臨時物件,假設是t;然後再將pi和t進行相乘,得到最終結果並賦值給s;最終銷燬臨時變數t,所以這背後是隱藏著一個臨時物件的建立和刪除的。

當然這裡一行程式碼可能感覺不到啥,假設我們要計算很多很多個半徑對應的面積呢?顯然需要寫for迴圈,如果迴圈一萬次就意味著要建立和銷燬臨時物件各一萬次。

因此,如果每一次建立物件都需要分配記憶體,銷燬物件時需要回收記憶體的話,那麼大量臨時物件的建立和銷燬就意味著要伴隨大量的記憶體分配以及回收操作,這顯然是無法忍受的,更何況Python的for迴圈本身就已經夠慢了。

因此Python在浮點數物件被銷燬後,並不急著回收物件所佔用的記憶體,換句話說其實物件還在,只是將該物件放入一個空閒的連結串列中。因為我們說物件可以理解為就是一片記憶體空間,物件如果被銷燬,那麼理論上記憶體空間要歸還給作業系統,或者回到記憶體池中;但Python考慮到效率,並沒有真正的銷燬物件,而是將物件放入到連結串列中,佔用的記憶體還在;後續如果再需要建立新的浮點數物件時,那麼從連結串列中直接取出之前放入的物件(我們認為被回收的物件),根據新的浮點數物件重新初始化對應的成員即可,這樣就避免了記憶體分配造成的開銷。而這個連結串列就是我們說的快取池,當然不光浮點數物件有快取池,Python中的很多其它物件也有對應的快取池,比如列表。

浮點物件的空閒連結串列同樣在 Objects/floatobject.c中定義:

#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST    100  
#endif 
static int numfree = 0;  
static PyFloatObject *free_list = NULL;
  • PyFloat_MAXFREELIST:快取池中能容納float例項物件的最大數量, 顯然不可能將所有要銷燬的物件都放入到快取池中, 這裡是100個;
  • numfree:表示當前快取池(連結串列)中的已經存在的float例項物件的數量, 初始為0;
  • free_list: 指向連結串列頭結點的指標, 連結串列裡面儲存的都是PyFloatObject, 所以頭節點的指標就是PyFloatObject *

但是問題來了,如果是通過連結串列來儲存的話,那麼物件肯定要有一個指標,來指向下一個物件,但是浮點數物件內部似乎沒有這樣的指標啊。是的,因為Python是使用內部的ob_type來指向下一個物件,本來ob_type指向的應該是PyFloat_Type,但是在連結串列中指向的是下一個PyFloatObject。

所以我們再回過頭來看看PyFloat_FromDouble:

PyObject *
PyFloat_FromDouble(double fval)
{	
    //顯然op是快取池中第一個PyFloatObject的指標
    PyFloatObject *op = free_list;
    if (op != NULL) {
        // 這個時候連結串列中的第一個物件已經被取出來重新分配了,顯然free_list要指向下一個PyFloatObject
        //我們說在連結串列中,ob_type被用於指向連結串列中的下一個PyFloatObject,換言之ob_type儲存的是下一個PyFloatObject的地址
        //但ob_type雖然儲存的是PyFloatObject的地址,但它的型別仍是struct _typeobject *, 或者說PyTypeObject *
        //所以在儲存的時候,下一個PyFloatObject *一定是先轉成了struct _typeobject *之後,再交給的ob_type,因為對於指標來說,是可以任意轉化的
        //所以Py_TYPE(op)獲取下一個物件的指標之後,還要再轉成PyFloatObject *,然後交給free_list儲存
        //如果沒有下一個物件了,那麼free_list就是NULL
        //因此在下一次分配的時候,上面if (op != NULL)就不成立了,因此會走下面的else,使用PyObject_MALLOC重新分配記憶體
        free_list = (PyFloatObject *) Py_TYPE(op); 
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    //.......
    return (PyObject *) op;
}

我們說物件建立時,會先從快取池中獲取。既然建立時可以從快取池獲取,那麼銷燬的時候,肯定要放入到快取池中,你要先放,之後才能取。而銷燬物件會呼叫型別物件的解構函式tp_dealloc,對於浮點數而言就是float_dealloc,我們看一下原始碼,同樣位於Objects/floatobject.c中。

static void
float_dealloc(PyFloatObject *op)
{	
    if (PyFloat_CheckExact(op)) {
        //如果numfree(當前快取池中float例項物件的數量)達到了快取池的最大容量
        if (numfree >= PyFloat_MAXFREELIST)  {
            //那麼呼叫PyObject_FREE回收物件所佔記憶體
            PyObject_FREE(op);
            return;
        }
        //否則的話,說明沒有達到最大容量限制,顯然此時不會真的銷燬物件,而是將其放入快取池中
        //將numfree加1
        numfree++;
        //我們說free_list指向連結串列的第一個元素,而這裡是獲取了op的ob_type,讓其指向free_list,即連結串列中的第一個元素
        //顯然在將物件放入連結串列中的時候,是放在連結串列的頭部位置
        //但我們說ob_type的型別是struct _typeobject *,所以還要將free_list進行轉化
        //那麼顯然在獲取的時候,還要再轉成PyFloatObject *,這在上面的PyFloat_FromDouble中我們已經看到了
        Py_TYPE(op) = (struct _typeobject *)free_list;
        //我們說free_list指向連結串列中的第一個元素,但現在第一個元素變了
        //所以要讓free_list = op, 指向新新增的PyFloatObject,因為它被插入到了連結串列的第一個位置上
        free_list = op;
    }
    //否則的話,說明PyFloat_CheckExact(op)為假, PyFloat_CheckExact(op)是用於檢測op是不是指向PyFloatObject
    //說明此時op可能指向的其實不是PyFloatObject *,所以通過Py_TYPE(op)->tp_free直接獲取對應的型別物件的tp_free,然後釋放掉op指向的物件所佔的記憶體。
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
}

這便是Python的浮點數物件(或者浮點數空閒物件)快取池的全部祕密,由於物件快取池在提高物件分配效率方面發揮著至關重要的作用,所以Python中很多其它內建物件的例項物件也都實現了快取池,我們後續在分析其它物件的時候會經常看到它的身影。

看一個思考題:

>>> a = 1.414
>>> id(a)
2431274355248
>>>
>>> del a
>>>
>>> b = 1.732
>>> id(b)
2431274355248
>>>

我們看到兩個物件的id是一樣的,相信你肯定知道原因。因為a在del之後,指向物件被放入到快取池中,然後建立b的時候會從快取池中獲取,所以a指向的物件被重新利用了,記憶體還是原來的那一塊記憶體,所以前後地址沒有變化。

物件的行為

PyFloat_Type中定義了很多的函式指標,比如:type_repr、tp_str、tp_hash等等,這些函式指標將一起決定float例項物件的行為,例如:tp_hash決定float例項物件的雜湊值是如何計算的:

>>> e = 2.71
>>> hash(e)
1637148536541722626
>>>

tp_hash指向的是float_hash,還是那句話Python底層的函式命名以及API都是很有規律的,相信你能慢慢發現。

static Py_hash_t
float_hash(PyFloatObject *v)
{	
    //我們看到呼叫了_Py_HashDouble,計算的就是ob_fval成員雜湊值
    return _Py_HashDouble(v->ob_fval);
}

由於加減乘除等數值操作很常見, Python 將其抽象成數值操作簇 PyNumberMethods,並讓內部成員tp_as_number指向。數值操作集 PyNumberMethods 在標頭檔案 Include/object.h 中定義:

typedef struct {
    /* Number implementations must check *both*
    arguments for proper type and implement the necessary conversions
    in the slot functions themselves. */

    binaryfunc nb_add;
    binaryfunc nb_subtract;
    binaryfunc nb_multiply;
    binaryfunc nb_remainder;
    binaryfunc nb_divmod;
    ternaryfunc nb_power;
    unaryfunc nb_negative;
    // ...

    binaryfunc nb_inplace_add;
    binaryfunc nb_inplace_subtract;
    binaryfunc nb_inplace_multiply;
    binaryfunc nb_inplace_remainder;
    ternaryfunc nb_inplace_power;
    //...
} PyNumberMethods;

PyNumberMethods定義了各種數學運算元的處理函式,數值計算最終由這些函式執行。 處理函式根據引數個數可以分為: 一元函式(unaryfunc) 、 二元函式(binaryfunc) 和 三元函式(ternaryfunc )。

然後我們回到Objects/floatobject.c中觀察一下PyFloat_Type是如何初始化的。

static PyNumberMethods float_as_number = {
    float_add,          /* nb_add */
    float_sub,          /* nb_subtract */
    float_mul,          /* nb_multiply */
    float_rem,          /* nb_remainder */
    float_divmod,       /* nb_divmod */
    float_pow,          /* nb_power */
    (unaryfunc)float_neg, /* nb_negative */
    // ...

    0,                  /* nb_inplace_add */
    0,                  /* nb_inplace_subtract */
    0,                  /* nb_inplace_multiply */
    0,                  /* nb_inplace_remainder */
    0,                  /* nb_inplace_power */
    // ...
};

以加法為例,顯然最終執行float_add,原始碼位於Objects/floatobject.c中,顯然它是一個二元函式。

static PyObject *
float_add(PyObject *v, PyObject *w)
{	
    //顯然兩個Python物件相加,一定是先將其轉成C的物件相加,加完之後再根據結果建立新的Python物件
    //所以聲明瞭兩個double
    double a,b;
    //CONVERT_TO_DOUBLE是一個巨集,不用想,功能肯定是將PyFloatObject裡面的ob_fval抽出來給double變數,從名字上也能看出來
    //這個巨集有興趣可以去原始碼中看一下,也在當前檔案中
    CONVERT_TO_DOUBLE(v, a);  // 將ob_fval賦值給a
    CONVERT_TO_DOUBLE(w, b);  // 將ob_fval賦值給b
    
    //PyFPE_START_PROTECT和下面的PyFPE_END_PROTECT也都是巨集,作用我們一會兒說。
    PyFPE_START_PROTECT("add", return 0)
    //將a和b相加賦值給a
    a = a + b;
    PyFPE_END_PROTECT(a)
    //根據相加後的結果建立新的PyFloatObject物件,當然返回的是泛型指標PyObject *
    return PyFloat_FromDouble(a);
}

所以以上就是float例項物件的運算,核心就是:

  • 1. 定義兩個double變數:a、b
  • 2. 將用來相加的兩個float例項物件中ob_fval維護的值抽出來賦值給a和b
  • 3. 讓a和b相加,將相加結果傳入PyFloat_FromDouble中建立新的PyFloatObject,然後返回其PyObject *

所以如果是C中的兩個浮點數相加,直接a + b就可以了,編譯之後就是一條簡單的機器指令,然而Python則需要額外做很多其它工作。並且在介紹整型的時候,你會發現Python中的整型的相加會更麻煩,但對於C而言同樣是一條簡單的機器碼就可以搞定。當然啦,因為Python3中的整型是不會溢位的,所以需要額外的一些處理,等介紹整型的時候再說吧。所以這裡我們也知道Python為什麼會比C慢幾十倍了,從一個簡單的加法上面就可以看出來。

最後我們再說一下PyFPE_START_PROTECT和PyFPE_END_PROTECT這兩個巨集,其實它們對於我們瞭解浮點數在底層的計算沒有什麼意義。首先浮點數計算一般都遵循IEEE-754標準,如果計算時出現了錯誤,那麼需要將IEEE-754異常轉換成Python中的異常,而這兩個巨集就是用來幹這件事情的。

所以我們不需要管它,在兩個巨集定義在Include/pyfpe.h中,並且Python3.9的時候會被刪除掉。

最後我們說一下Python直譯器原始碼的結構吧,因為我們每一次介紹函式的時候,都會說該函式定義在哪個檔案裡。所以突然想起來,介紹一下原始碼的組織結構也是有必要的。

我們從官網上將原始碼下載下來之後,大概長這樣,裡面有幾個目錄是我們需要關注的。

  • Include:該目錄包含了Python所提供的所有標頭檔案,主要包含了一些例項物件在底層的定義,比如listobject.h、dictobject.h等等。如果使用者需要自己使用C或者C++來編寫自定義模組擴充套件Python,那麼也需要用到這裡的標頭檔案。
  • Lib:這個無需多說,該目錄包含了python自帶的所有標準庫,Lib中的庫基本上都是使用python編寫的。
  • Modules:該目錄中包含了所有用C語言編寫的模組,比如_random、_io等,而且gc也在裡面。Modules中的模組是那些對速度要求非常嚴格的模組,而有一些對速度沒有太嚴格要求的模組,比如os,就是用Python編寫,並且是放在Lib目錄下的。
  • Parser:該目錄中包含了python直譯器中的Scanner和Parser部分,即對python原始碼進行詞法分析和語法分析的部分。除了這些,Parser還包含了一些有用的工具,這些工具能夠根據Python語言的語法自動生成Python語言的詞法和語法分析器,與YACC非常類似。
  • Objects:該目錄包含了所有Python的內建型別物件的實現,以及其例項物件相關操作的實現,比如浮點數相關操作就位於檔案floatobject.c中、列表相關操作就位於檔案listobject.c中,檔名也很有規律。同時,該目錄還包含了Python在執行時需要的所有內部使用物件的實現,因為有很多物件比如<class 'function'>是沒有暴露給Python的,但是在底層它們是實現了的。
  • Python:虛擬機器的實現相關,是python執行的核心所在。

PyFloatObjectに侵入し

最後我們修改一下原始碼:當物件放入到緩衝池中,我們列印一下放入的浮點數物件的地址;當物件從快取池中取出時,我們列印一下取出的浮點數物件的地址。

我們看到在直譯器剛啟動的時候,內部就已經創建出很多物件了,然後我們自己來建立一個物件吧。

我們第一次建立物件的時候,居然是從快取池裡面獲取的,說明在直譯器啟動的時候那個連結串列中就已經有空閒物件了。然後我們使用Python獲取其id,由於得到的是十進位制整型,所以轉成16進位制,發現地址是一樣的。然後放入到快取池中,放入的物件的地址也是相同的,這和我們得到結論是一致的。

我們再建立新的變數a、b並列印地址,然後刪除a、b變數,再重新建立a、b變數、列印地址,結果發現它們儲存的物件的地址在刪除前後正好是相反的。至於原因,如果思考一下將物件放入快取池、以及從快取池獲取物件的時候所採取的策略,那麼很容易就明白了。

因為del a, b的時候會先刪除a,再刪除b。刪除a的時候,會將a指向的物件作為連結串列中的第一個元素,然後刪除b的時候,會將b指向的物件作為連結串列中的第一個元素,所以之前a指向的物件就變成了連結串列中的第二個元素。而獲取的時候,也會從連結串列的頭部開始獲取,所以當重新建立變數a的時候,其指向的物件實際上使用的是之前變數b指向的物件所佔的記憶體,而一旦獲取,那麼free_list指標會向後移動;因此建立變數b的時候,其指向的物件顯然使用的是之前變數a指向的物件所佔的記憶體。因此前後列印的地址是相反的,所以我們算是通過實踐從另一個角度印證了之前分析的結論。

小結

這一篇我們分析了Python中的浮點數在底層的實現方式,之所以選擇浮點數是因為浮點數是最簡單的了。至於整數,其實並沒有那麼簡單,因為它的值底層是通過陣列儲存的,而浮點型底層是用一個double儲存對應的值,所以更簡單一些,我們就先拿浮點數"開刀了"。

然後我們還介紹浮點數的建立和銷燬,會呼叫型別物件內部的tp_dealloc,浮點數的話就是float_dealloc,當然整型肯定就是long_dealloc了。當然為了保證效率,避免記憶體的建立和回收,Python底層為浮點數引入了快取池機制,我們也分析了它的機制。當然浮點數還支援相關的數值型操作,PyFloat_Type中的tp_as_number指向了PyNumberMethods結構體,裡面有大量的函式指標,每個指標指向了具體的函式,專門用於浮點數的運算。當然整型也有,只不過指標指向的函式是用於整型運算的。比如相加:對於浮點數來說,PyNumberMethods結構體成員nb_add指向了函式float_add;對於整數來說,nb_add則是指向了long_add。然後我們也以相加為例,看了float_add函式的實現,核心就是將Python中物件的值抽出來,轉成C的型別,然後運算,最後再根據運算的結果,建立Python中的物件、返回。當然除了加法,它的減法、乘法、除法都是類似的,有興趣可以"殺入"floatobject.c中,大肆探索一番。

最後我們修改了PyFloatObject的部分原始碼,其實就是加上了兩個printf語句,對float例項物件的快取池機制進行了實踐,並用之前的結論對結果進行了合理的解釋。