使用ctypes探索Python Object内部结构

在Python中我们可以直接使用dir函数获取一个对象的内部数据(推荐使用inspect.getmembers,输出内容更为详细),但是想知道这个对象在内存级别上的结构时此方法就无能为力了。

常用的一个Python实现Cpython中,一个对象的实现(PyObject)其实是一个结构体。那要探索结构体的方法就很多了,比如重新编译解释器,在其中输出log,或者直接使用gdb。

还有另外一个方案,就是使用内置的ctypes。ctypes可以将任意一段数据转换为C的结构体,处理PyObject当然也没问题啦。

PyObject的定义在object.h当中,在2.7版本,并且没有定义Py_TRACE_REFS的情况下,将宏展开后是这个样子:

typedef struct _object {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

用ctypes表示的话如下:

class PyObject(ctypes.Structure):
    _fields_ = [
        ("ob_refcnt", ctypes.c_size_t),
        ("ob_type", ctypes.c_void_p)
    ]

Python内置函数id的返回值其实是一个对象在内存中的地址,于是我们就可以这样拿到一个对象的PyObject:

>> i = 42

>> o = PyObject.from_address(id(i))

ob_refcnt是一个对象的引用计数,ob_type是对象的类型,是一个TypeObject,由Python在运行时生成,我们可以在types模块中拿到其引用,因此:

>> o.ob_refcnt
=> 1L    # 变量`i`持有此引用

>> del i

>> o.ob_refcnt
=> 0L

>> o.ob_type
=> 1810418784

>> assert id(types.IntType) == o.ob_type

>> del i

int对象的定义在intobject.h中,将其宏展开之后如下:

typedef struct _object {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    long ob_ival;
} PyIntObject;

因此:

class PyIntObject(PyObject):
    _fields_ = [
        ("ob_ival", ctypes.c_long),
    ]

PyIntObject.from_address(id(1)).ob_ival # => 1
另外list与dict等变长对象,在C的结构体中肯定无法直接表示变长字段,因此这类对象统一用PyVarObject表示,PyListObject的表示如下:

class PyVarObject(PyObject):
    _fields_ = [
       ("ob_size", ctypes.c_size_t),
    ]

class PyListObject(PyVarObject):
    _fields_ = [
        ("ob_item", ctypes.c_void_p),
        ("allocated", ctypes.c_size_t),
    ]

其中ob_size为变长对象包含对象的数目,ob_item为一个指向PyObject的指针,也就是具体内容存放的地方,allocated是list有时会预先分配一部分位置为了预留以后添加新元素:

>> l = range(42)

>> o = PyListObject.from_address(id(l))
=> <objs.PyVarObject at 0x9fa6311c>

>> o.ob_size
=> 42L

>> o.allocated
=> 42L

参考:Common Object Structures