转载地址:http://hyry.dip.jp/tech/slice/slice.html/10html
在 Python 中一切皆是对象,而在实现 Python 的 C
语言中,这些对象只不过是一些比较复杂的结构体而已。本文经过 ctypes
访问对象对应的结构体中的数据,加深对 Python 对象的理解。数组
Python 全部对象结构体中的头两个字段都是相同的:app
refcnt
:对象的引用次数,若引用次数为 0
则表示此对象能够被垃圾回收了。ui
typeid
:指向描述对象类型的对象的指针。this
经过 ctypes
,咱们能够很容易定义一个这样的结构体:PyObject
。操作系统
注意:本文只描述在
32 位操做系统下的状况,若是读者使用的是
64 位操做系统,须要对程序中的一些字段类型作一些改变。
指针
from ctypes import * class PyObject(Structure): _fields_ = [("refcnt", c_size_t), ("typeid", c_void_p)]
下面让咱们用 PyObject
作一些实验帮助理解这两个字段的含义:code
>>> 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)
。htm
下面查看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)>
能够看出大指针数组的位置没有发生变化,可是后面额外的空间被回收了。