如何打印Python数据类型内存结构 输出内部字段十六进制转储
调试器内置工具(准确率最高)
如果可以用GDB/LLDB附加到Python进程,CPython源码自带的调试辅助脚本是最匹配需求的选择,和当前运行的Python版本结构体定义完全一致:
- 原生支持解析所有内置类型的内部字段,包括引用计数、类型指针、变长对象长度、列表的
allocated和ob_item、字典哈希表结构等所有内部细节 - 自带递归解析引用对象的能力,可直接展示每个字段的内存地址、实际值
- 不需要额外安装第三方库,只要调试环境的Python版本和运行版本匹配、带基础调试符号即可使用
缺点是需要在调试器环境下操作,无法直接在普通Python代码中调用输出。
纯Python第三方库
目前没有完全匹配需求(同时实现结构体字段自动解析+格式化十六进制转储+递归展示引用对象)的成熟库,可组合使用以下接近的工具:
hexdump:专门做内存/字节串的格式化十六进制输出,输出格式和给出的示例完全一致,支持自定义每行字节数、偏移起始地址、ASCII展示列,传入内存起始地址和长度即可得到标准的十六进制转储内容。objprint:可递归打印任意Python对象的内存地址、属性值、内部嵌套结构,支持自定义输出深度,但默认不输出原始十六进制内存内容,可以和hexdump组合使用补全内存转储能力。pyinternals:面向教学场景的小型工具库,内置了常见CPython内置类型(int、str、list、dict、bytes等)的结构体定义,可以直接读取并打印对象头的引用计数、类型指针、各类型特有字段的值,缺点是更新不及时,对3.11+版本调整过的结构体(比如新版字符串、字典布局)支持不好,需要手动适配结构体定义。
如果要做适配自身Python版本、完全符合输出要求的工具,开发成本很低,核心步骤如下:
定义对应版本的CPython结构体:找到当前运行的CPython版本对应源码里的头文件(比如
listobject.h、object.h、dictobject.h等),用ctypes.Structure按顺序定义对应字段,64位环境下Py_ssize_t对应c_ssize_t、指针对应c_void_p,ctypes默认内存对齐规则和CPython编译时一致即可。
64位CPython 3.10环境下PyListObject的定义示例:from ctypes import Structure, c_ssize_t, c_void_p, POINTER class PyObject(Structure): _fields_ = [ ("ob_refcnt", c_ssize_t), ("ob_type", c_void_p), ] class PyVarObject(Structure): _fields_ = [ ("ob_base", PyObject), ("ob_size", c_ssize_t), ] class PyListObject(Structure): _fields_ = [ ("ob_base", PyVarObject), ("ob_item", POINTER(c_void_p)), ("allocated", c_ssize_t), ]实现通用十六进制转储函数:输入内存起始地址、长度,按每行16字节的格式输出偏移、十六进制字节、对应可打印ASCII字符即可,几十行代码就能实现,不需要依赖第三方库。
编写类型分发的递归解析逻辑:拿到任意对象的地址后,先转成对应类型的结构体指针,打印对象地址、引用计数、类型指针、类型特有字段,再根据类型递归解析其引用的其他对象(比如列表就遍历
ob_item指向的指针数组,逐个解析每个元素),同时对每个对象的内存块调用十六进制转储函数输出原始内存内容。核心代码量一般在200行以内就能覆盖list、dict、str、int、bytes这些最常用的内置类型,足够用于教学和底层原理探究。
- 你当前写的示例代码存在缺陷:
sys.getsizeof(a)只返回PyListObject结构体本身的大小,不包含ob_item指向的元素指针数组、以及元素对象本身占用的内存,所以只能打印列表头的内容,看不到元素指针和元素数据。 - 只要保证转储过程中对象被正常持有(不会被GC回收),只读访问内存是完全安全的,不要随意修改内存内容就不会导致解释器崩溃。
- 不同CPython大版本的结构体布局差异很大(比如3.11之后的字符串结构、3.12之后的字典结构都有调整),一定要用对应版本的头文件定义结构体,否则解析出来的字段值会出错。
内容的提问来源于stack exchange,提问作者martin's

