使用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