用ctypes觀察Python物件的記憶體結構

發表於2016-06-02

在 Python 中一切皆是物件,而在實現 Python 的 C 語言中,這些物件只不過是一些比較複雜的結構體而已。本文通過 ctypes 訪問物件對應的結構體中的資料,加深對 Python 物件的理解。

物件的兩個基本屬性

Python 所有物件結構體中的頭兩個欄位都是相同的:

  • refcnt:物件的引用次數,若引用次數為 0 則表示此物件可以被垃圾回收了。
  • typeid:指向描述物件型別的物件的指標。

    通過 ctypes,我們可以很容易定義一個這樣的結構體:PyObject

    注意:本文只描述在 32 位作業系統下的情況,如果讀者使用的是 64 位作業系統,需要對程式中的一些欄位型別做一些改變。

下面讓我們用 PyObject 做一些實驗幫助理解這兩個欄位的含義:

❶通過 id(a) 可以獲得物件 a 的記憶體地址,而 PyObject.from_address()可以將指定的記憶體地址的內容轉換為一個 PyObject 物件。通過此 PyObject 物件obj_a 可以訪問物件 a 的結構體中的內容。
❷檢視物件 a 的引用次數,由於只有 a 這個名字引用它,因此值為 1。接下來建立一個列表,此列表中的每個元素都是物件 a,因此此列表應用了它 10 次,❸所以引用次數變為了 11。
❸檢視物件 a 的型別物件的地址,它和 id(type(a)) 相同,而由於物件a的型別為str,因此也就是 id(str)

下面檢視str型別物件的這兩個欄位:

可以看到 str 的型別就是type。再看看 type 物件:

type 物件的型別指標就指向它自己,因為 type(type) is type

整數和浮點數物件

接下來看看整數和浮點數物件,這兩個物件除了有 PyObject 中的兩個欄位之外,還有一個 val 欄位儲存實際的值。因此 Python 中一個整數佔用 12 個位元組,而一個浮點數佔用 16 個位元組:

我們無需重新定義 refcnttypeid 這兩個欄位,通過繼承 PyObject,可以很方便地定義整數和浮點數對應的結構體,它們會繼承父類中定義的欄位:

下面是使用 PyInt 檢視整數物件的例子:

通過 PyInt 物件,還可以修改整數物件的內容:
修改不可變物件的內容會造成嚴重的程式錯誤,請不要用於實際的程式中。

由於i和j引用的是同一個整數物件,因此i和j的值同時發生了變化。

結構體大小不固定的物件

表示字串和長整型數的結構體的大小不是固定的,這些結構體在 C 語言中使用了一種特殊的欄位定義技巧,使得結構體中最後一個欄位的大小可以改變。由於結構體需要知道最後一個欄位的長度,因此這種結構中包含了一個 size 欄位,儲存最後一個欄位的長度。在 ctypes 中無法表示這種長度不固定的欄位,因此我們使用了動態建立結構體類的方法。

❶在定義長度不固定的欄位時,使用長度為 0 的陣列定義一個不佔記憶體的偽欄位 _valcreate_var_object() 用來建立大小不固定的結構體物件,❷首先搜尋名為 _val 的欄位,並將其型別儲存到 inner_type 中。❸然後建立一個PyVarObject 結構體讀取obj物件中的 size 欄位。❹再通過 size 欄位的大小建立一個對應的 Inner 結構體類,它可以從 struct 繼承,因為 struct 中的 _val 欄位不佔據記憶體。
下面我們用上面的程式做一些實驗:

當整數的範圍超過了 0x7fffffff 時,Python 將使用長整型整數:

可以看到 Python 用了 4 個 16 位的整數表示 0x1234567890abcd,下面我們看看長整型數是如何用陣列表示的:

即陣列中的後面的元素表示高位,每個 16 為整數中有 15 位表示數值。

列表物件

列表物件的長度是可變的,因此不能採用字串那樣的結構體,而是使用了一個指標欄位items指向可變長度的陣列,而這個陣列本身是一個指向 PyObject 的指標。 allocated 欄位表示這個指標陣列的長度,而 size 欄位表示指標陣列中已經使用的元素個數,即列表的長度。列表結構體本身的大小是固定的。

我們用下面的程式檢視往列表中新增元素時,列表結構體中的各個欄位的變化:

執行 test_list() 得到下面的結果:

❶一開始列表的長度和其指標陣列的長度都是 3,即列表處於飽和狀態。因此❷往列表中新增新元素時,需要重新分配指標陣列,因此指標陣列的長度變為了 7,而地址也發生了變化。這時列表的長度為 4,因此指標陣列中還有 3 個空位儲存新的元素。由於每次重新分配指標陣列時,都會預分配一些額外空間,因此往列表中新增元素的平均時間複雜度為 O(1)

下面再看看從列表刪除元素時,各個欄位的變化:

執行test_list2()得到下面的結果:

可以看出大指標陣列的位置沒有發生變化,但是後面額外的空間被回收了。

相關文章