资讯专栏INFORMATION COLUMN

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

smallStone / 2244人阅读

摘要:转载地址在中一切皆是对象,而在实现的语言中,这些对象只不过是一些比较复杂的结构体而已。由于和引用的是同一个整数对象,因此和的值同时发生了变化。用来创建大小不固定的结构体对象,首先搜索名为的字段,并将其类型保存到中。

转载地址: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   ❶
4 7   ❷
5 7 
6 7 
7 7 
8 12 
9 12 
10 12 
11 12 
12 12 

❶一开始列表的长度和其指针数组的长度都是 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 
10 17 

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/37606.html

相关文章

  • 不要迷恋我,我只是利Python修改了游戏内存

    摘要:上篇文章我许了一个愿,就是想让大家多多关注我,然后我的粉丝就蹭蹭的涨了好几百,谢谢大家的厚爱。可是我发现粉丝是涨了,三连变少了,谢谢大家这次给我三连,我一定再接再厉。地址的寻找阳光总值,种植一个豌豆需要,非常不够用。 目录 前言 游戏的安装 思路       一句话总结       大概的思...

    ermaoL 评论0 收藏0
  • Python C 动态链接库,包括结构体参数、回调函数等

    摘要:调用以回调函数地址为参数的函数这个主题就稍微绕一些了,也就是说在接口中,需要传入回调函数作为参数。这个问题在中也可以解决,并且回调函数可以用定义。代码代码很简单回调函数的传入参数为,返回参数也是。 项目中要对一个用 C 编写的 .so 库进行逻辑自测。这项工作,考虑到灵活性,我首先考虑用 Python 来完成。 研究了一些资料,采用 python 的 ctypes 来完成这项工作。已经...

    NickZhou 评论0 收藏0
  • Python 外部函数调ctypes简介

    摘要:最近了解了提供的一个外部函数库它提供了语言兼容的几种数据类型,并且可以允许调用编译好的库。这里是阅读相关资料的一个记录,内容大部分来自官方文档。注意,提供的接口会在不同系统上有出入,比如为了加载动态链接库,在上提供的是而在上提供的是和。 参考资料 https://docs.python.org/2.7/l... http://www.ibm.com/developerw... c...

    mykurisu 评论0 收藏0
  • SWIG 对 C++ 库进行 Python 包装

    摘要:可以在接口文件中直接引用库里的内容,大大方便接口文件的编写。使用库里的这里先介绍方式通过创建出来的数组是数组的直接代理,非常底层和高效,但是,它也和数组一样不安全,一样没有边界检查。对由于这种情况,可以使用库里的。 如果你也像我们一样,同时使用Python和C++,以获得两种语言的优势,一定也会希望寻找一种好的方式集成这两种语言,相比而言,让Python能够方便使用C++的库更加重要,...

    jas0n 评论0 收藏0
  • Python进程专题6:共享数据与同步

    摘要:可以使用标准的索引切片迭代操作访问它,其中每项操作均锁进程同步,对于字节字符串,还具有属性,可以把整个数组当做一个字符串进行访问。当所编写的程序必须一次性操作大量的数组项时,如果同时使用这种数据类型和用于同步的单独大的锁,性能将极大提升。 上一篇文章:Python进程专题5:进程间通信下一篇文章:Python进程专题7:托管对象 我们现在知道,进程之间彼此是孤立的,唯一通信的方式是队...

    Yuanf 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<