[轉載] 用ctypes觀察Python對象的內存結構

轉載地址:http://hyry.dip.jp/tech/slice/slice.html/10html

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

對象的兩個基本屬性

Python 全部對象結構體中的頭兩個字段都是相同的:app

  • refcnt:對象的引用次數,若引用次數爲 0 則表示此對象能夠被垃圾回收了。ui

  • typeid:指向描述對象類型的對象的指針。this

    經過 ctypes,咱們能夠很容易定義一個這樣的結構體:PyObject操作系統

    注意:本文只描述在 32 位操做系統下的狀況,若是讀者使用的是 64 位操做系統,須要對程序中的一些字段類型作一些改變。指針

from ctypes import *

class PyObject(Structure):
    _fields_ = [("refcnt", c_size_t),
                ("typeid", c_void_p)]

下面讓咱們用 PyObject 作一些實驗幫助理解這兩個字段的含義:code

>>> a = "this is a string"
>>> obj_a = PyObject.from_address(id(a)) ❶
>>> obj_a.refcnt ❷
1L
>>> b = [a]*10
>>> obj_a.refcnt ❸
11L
>>> obj_a.typeid ❹
505269056
>>> id(type(a))
505269056
>>> id(str)
505269056

❶經過 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)htm

下面查看str類型對象的這兩個字段:對象

>>> obj_str = PyObject.from_address(id(str))
>>> obj_str.refcnt
252L
>>> obj_str.typeid
505208152
>>> id(type)
505208152

能夠看到 str 的類型就是type。再看看 type 對象:

>>> type_obj = PyObject.from_address(id(type))
>>> type_obj.typeid
505208152

type 對象的類型指針就指向它本身,由於 type(type) is type

整數和浮點數對象

接下來看看整數和浮點數對象,這兩個對象除了有 PyObject 中的兩個字段以外,還有一個 val 字段保存實際的值。所以 Python 中一個整數佔用 12 個字節,而一個浮點數佔用 16 個字節:

>>> sys.getsizeof(1)
12
>>> sys.getsizeof(1.0)
16

咱們無需從新定義 refcnttypeid 這兩個字段,經過繼承 PyObject,能夠很方便地定義整數和浮點數對應的結構體,它們會繼承父類中定義的字段:

class PyInt(PyObject):
    _fields_ = [("val", c_long)]

class PyFloat(PyObject):
    _fields_ = [("val", c_double)]

下面是使用 PyInt 查看整數對象的例子:

>>> i = 2000
>>> i_obj = PyInt.from_address(id(a))
>>> i_obj.refcnt
1L
>>> i_obj.val
2000

經過 PyInt 對象,還能夠修改整數對象的內容:
修改不可變對象的內容會形成嚴重的程序錯誤,請不要用於實際的程序中。

>>> j = i
>>> i_obj.val = 2012
>>> j
2012

因爲i和j引用的是同一個整數對象,所以i和j的值同時發生了變化。

結構體大小不固定的對象

表示字符串和長整型數的結構體的大小不是固定的,這些結構體在 C 語言中使用了一種特殊的字段定義技巧,使得結構體中最後一個字段的大小能夠改變。因爲結構體須要知道最後一個字段的長度,所以這種結構中包含了一個 size 字段,保存最後一個字段的長度。在 ctypes 中沒法表示這種長度不固定的字段,所以咱們使用了動態建立結構體類的方法。

class PyVarObject(PyObject):
    _fields_ = [("size", c_size_t)]

class PyStr(PyVarObject):
    _fields_ = [("hash", c_long),
                ("state", c_int),
                ("_val", c_char*0)]  ❶

class PyLong(PyVarObject):
    _fields_ = [("_val", c_uint16*0)]

def create_var_object(struct, obj):
    inner_type = None
    for name, t in struct._fields_:
        if name == "_val":                      ❷
            inner_type = t._type_
    if inner_type is not None:
        tmp = PyVarObject.from_address(id(obj))  ❸
        size = tmp.size
        class Inner(struct):              ❹
            _fields_ = [("val", inner_type*size)]
        Inner.__name__ = struct.__name__
        struct = Inner
    return struct.from_address(id(obj))

❶在定義長度不固定的字段時,使用長度爲 0 的數組定義一個不佔內存的僞字段 _valcreate_var_object() 用來建立大小不固定的結構體對象,❷首先搜索名爲 _val 的字段,並將其類型保存到 inner_type 中。❸而後建立一個PyVarObject 結構體讀取obj對象中的 size 字段。❹再經過 size 字段的大小建立一個對應的 Inner 結構體類,它能夠從 struct 繼承,由於 struct 中的 _val 字段不佔據內存。
下面咱們用上面的程序作一些實驗:

>>> s_obj = create_var_object(PyStr, s)
>>> s_obj.size
9L
>>> s_obj.val
'abcdegfgh'

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

>>> l = 0x1234567890abcd
>>> l_obj = create_var_object(PyLong, l)
>>> l_obj.size
4L
>>> val = list(l_obj.val)
>>> val
[11213, 28961, 20825, 145]

能夠看到 Python 用了 4 個 16 位的整數表示 0x1234567890abcd,下面咱們看看長整型數是如何用數組表示的:

>>> hex((val[3] << 45) + (val[2] << 30) + (val[1] << 15) + val[0])
'0x1234567890abcdL'

即數組中的後面的元素表示高位,每一個 16 爲整數中有 15 位表示數值。

列表對象

列表對象的長度是可變的,所以不能採用字符串那樣的結構體,而是使用了一個指針字段items指向可變長度的數組,而這個數組自己是一個指向 PyObject 的指針。 allocated 字段表示這個指針數組的長度,而 size 字段表示指針數組中已經使用的元素個數,即列表的長度。列表結構體自己的大小是固定的。

class PyList(PyVarObject):
    _fields_ = [("items", POINTER(POINTER(PyObject))),
                ("allocated", c_size_t)]

    def print_field(self):
        print self.size, self.allocated, byref(self.items[0])

咱們用下面的程序查看往列表中添加元素時,列表結構體中的各個字段的變化:

def test_list():
    alist = [1,2.3,"abc"]
    alist_obj = PyList.from_address(id(alist))

    for x in xrange(10):
        alist_obj.print_field()
        alist.append(x)

運行 test_list() 獲得下面的結果:

>>> test_list()
3 3 <cparam 'P' (02B0ACE8)>  ❶
4 7 <cparam 'P' (028975A8)>  ❷
5 7 <cparam 'P' (028975A8)>
6 7 <cparam 'P' (028975A8)>
7 7 <cparam 'P' (028975A8)>
8 12 <cparam 'P' (02AAB838)>
9 12 <cparam 'P' (02AAB838)>
10 12 <cparam 'P' (02AAB838)>
11 12 <cparam 'P' (02AAB838)>
12 12 <cparam 'P' (02AAB838)>

❶一開始列表的長度和其指針數組的長度都是 3,即列表處於飽和狀態。所以❷往列表中添加新元素時,須要從新分配指針數組,所以指針數組的長度變爲了 7,而地址也發生了變化。這時列表的長度爲 4,所以指針數組中還有 3 個空位保存新的元素。因爲每次從新分配指針數組時,都會預分配一些額外空間,所以往列表中添加元素的平均時間複雜度爲 O(1)

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

def test_list2():
    alist = [1] * 10000
    alist_obj = PyList.from_address(id(alist))

    alist_obj.print_field()
    del alist[10:]
    alist_obj.print_field()

運行test_list2()獲得下面的結果:

>>> test_list2()
10000 10000 <cparam 'P' (034E5AB8)>
10 17 <cparam 'P' (034E5AB8)>

能夠看出大指針數組的位置沒有發生變化,可是後面額外的空間被回收了。

相關文章
相關標籤/搜索