zoukankan      html  css  js  c++  java
  • Python 用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
    

    我们无需重新定义refcnt和typeid这两个字段,通过继承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的数组定义一个不占内存的伪字段_val。create_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)>
    

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

    文章来源:http://hyry.dip.jp/tech/slice/slice.html/10

  • 相关阅读:
    POJ 2891 Strange Way to Express Integers 中国剩余定理解法
    @Repository @Service 和@Autowired 的使用
    POJ 3070 Fibonacci
    无线局域网技术
    第一次ACM赛后总结及感悟
    Subsets II -- LeetCode
    [WebView学习之二]:使用Web Apps 支持不同分辨率屏
    微信公众平台开发新手教程(图文具体解释)
    翻译-In-Stream Big Data Processing 流式大数据处理
    Nginx + FastCgi + Spawn-fcgi + C 架构的server环境搭建
  • 原文地址:https://www.cnblogs.com/fendou-999/p/3535194.html
Copyright © 2011-2022 走看看