设为首页 收藏本站
查看: 867|回复: 0

[经验分享] 用ctypes观察Python对象的内存结构

[复制链接]

尚未签到

发表于 2017-5-6 11:50:36 | 显示全部楼层 |阅读模式
  对象的两个基本属性
  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 ❶
  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
  可以看出大指针数组的位置没有发生变化,但是后面额外的空间被回收了。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-373778-1-1.html 上篇帖子: 零基础学python-16.7 nonlocal介绍 下篇帖子: python中List的sort方法(或者sorted内建函数)的用法
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表