[转载] 用ctypes观察Python对象的内存结构

转载地址:http://hyry.dip.jp/tech/slice/slice.html/10

在 Python 中一切皆是对象,而在实现 Python 的 C 语言中,这些对象只不过是一些比较复杂的结构体而已。本文通过 ctypes 访问对象对应的结构体中的数据,加深对 Python 对象的理解。

对象的两个基本属性

Python 所有对象结构体中的头两个字段都是相同的:

  • refcnt:对象的引用次数,若引用次数为 0 则表示此对象可以被垃圾回收了。

  • typeid:指向描述对象类型的对象的指针。

    通过 ctypes,我们可以很容易定义一个这样的结构体:PyObject

    注意:本文只描述在 32 位操作系统下的情况,如果读者使用的是 64 位操作系统,需要对程序中的一些字段类型做一些改变。

from ctypes import *

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

下面让我们用 PyObject 做一些实验帮助理解这两个字段的含义:

>>> 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)

下面查看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)>

可以看出大指针数组的位置没有发生变化,但是后面额外的空间被回收了。


黑月亮
点滴记录,步步成长

现实与完美之间

1.6k 声望
24 粉丝
0 条评论
推荐阅读
centos | 修改静态 IP
设置 Centos 为使用静态 IP1 修改网络配置 {代码...} 修改后的内容如下 {代码...} 2 重启网络服务 {代码...} 3 查看地址 {代码...} 参考来源:[链接]

青阳半雪阅读 1.8k评论 3

数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.3k

滚蛋吧,正则表达式!
你是不是也有这样的操作,比如你需要使用「电子邮箱正则表达式」,首先想到的就是直接百度上搜索一个,然后采用 CV 大法神奇地接入到你的代码中?

良许3阅读 1.5k

搭个ChatGPT算法模型,从哪开始?
最近 ChatGPT 很火,火到了各行各业。记得去年更多的还是码农最新体验后拿它搜代码,现在各行各业都进来体验,问它咋理财、怎么写报告和给小孩起名。😂 也因此让小傅哥在头条的一篇关于 ChatGPT 的文章都有了26万...

小傅哥6阅读 1.1k

封面图
程序员适合创业吗?
大家好,我是良许。从去年 12 月开始,我已经在视频号、抖音等主流视频平台上连续更新视频到现在,并得到了不错的评价。每个视频都花了很多时间精力用心制作,欢迎大家关注哦~考虑到有些小伙伴没有看过我的视频,...

良许3阅读 1.3k

Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 4.5k评论 1

PyCharm 激活破解教程, 2023 年 2 月亲测有用
本文分享一下PyCharm 2022.2.3 版本最新激活破解教程,注意不要使用太新的版本,都是 Jetbrains 产品,本文专门配上了 Pycharm 的图片,跟着下面教程一步一步来即可。

程序员徐公阅读 8.1k评论 1

现实与完美之间

1.6k 声望
24 粉丝
宣传栏