slots - Python的结构体 转

时间:2023-02-12 14:33:36
 
 
 
上个月看了篇文章 “SAVING 9 GB OF RAM WITH PYTHON’S __SLOTS__”,原来Python也有类似结构体的东东。拖了一个月才写这篇,是因为太久没看python源码而生疏了,中间又捣鼓了一下tmux神马的。
简单的说,slots提供了一种强制声明对象属性的方法。如果在类定义的时候定义了__slots__的值(string列表),这个类的对象就只能使用列表中属性名。

class A(object):
     def __init__(self):
          self.uuid = 2
          self.word = ‘hello'

变成:

class B(object):
     __slots__ = ( 'value', ‘other')
     def __init__(self):
          self.uuid = 2
          self.word = ‘hello'
          self.other = ‘world'  # AttributeError: ‘B’ object has no attribute ‘other’

是不是很像C的结构体?注意__slots__只是起了限定的作用,属性还是要先赋值才读取。

引入slots的主要目的是节省内存。默认python会为每个对象创建一个dict,通过__dict__索引。脚本中定义的属性其实都存在这个dict中。如果类定义了__slots__,创建对象时就不会创建dict,这也就解释了为什么不能随意增加属性了。

能省多少内存呢?dict对象本身只有248bytes。而dict的容量通常是其中有效entry的2~4倍,使用slots不会减少有效entry的数量,因此大概能省一半的空entry。原文中能从25G内存中省下9G,应该是不仅对象很多,而且每个对象的属性很多。考虑到我们实际的应用环境,似乎达不到这样的量。而且服务端很多类都是在C中定义的,其实效果是一样的。

性能上会有提高吗?实测结果几乎没有,从实现上可以看出,其实还是需要做一次dict查找。

目前想到的一个用处是提高可维护性:虽然作为动态语言,允许随意增删对象的属性是个很方便的设定。但是也会对可维护性造成一定的影响。比如一不小心写错属性名。如果是赋值,python会默默的以为是一个新的,导致真正要改的数据没改到,如果是读取,一直到运行时跑到这段代码才会报错。通过定义slots,可以一定程度上避免错误赋值,但是对于错误读取就作用不大了。

关于slots的使用有一些规则和限制,在python自带的文档里有详细说明。

最后是实现的一些细节。代码主要在Objects/typeobject.c。下面两张图是普通类和使用slots的类的区别(红色部分)。

slots - Python的结构体  转

对于普通类A,我们可以看到,它的tp_dictoffset的值是一个偏移量,对应于这个类所创建的对象(比如a)内存中的从头部数到这个偏移量的内存地址,其实是一个PyObject指针。这个指针指向一个dict对象,也就是我们在运行期可以访问到的a.__dict__。脚本中定义的所有属性就保存在这个dict当中。由于这两个指针分别有8字节,因此整个对象a的大小是基本大小16加上8*2,总共32字节。

slots - Python的结构体  转

而对于使用了slots的类B,它的tp_dictoffset就变成了0。换句话说就是没有分配dict对象。那怎么索引到属性呢?其实是上升到了类的层次。在Python中,所有东西都是对象,包括类型本身也是对象,类型对象的类型是PyType_Type(脚本中的type)。听起来很绕,其实这里只是想说明,类型对象是变长对象,包括基本信息加上若干PyMemberDef对象。每个PyMemberDef对象其实就对应于slots中个每个属性声明。而这个类所创建的对象中,也会为每个属性声明分配一个PyObject指针的内存。在初始化类型的时候(PyType_Ready函数),系统会为每个PyMemberDef创建一个descriptor,并在类的__dict__(不是对象的__dict__,从类的tp_dict指向)中建立从属性名到descriptor的映射。

因此,无论哪种方法,都需要进行一次hash查找,区别只是在类的__dict__还是对象的__dict__中。

最后的最后,推荐一下这个不需要安装的在线画图工具:lucidchart

 
 
 
 
阅读(755)| 评论(0)