Python__slots__详解
摘要
当一个类需要创建大量实例时,可以通过__slots__
声明实例所需要的属性,
例如,class Foo(object): __slots__ = ['foo']
。这样做带来以下优点:
- 更快的属性访问速度
- 减少内存消耗
以下测试环境为Ubuntu16.04 Python2.7
Slots的实现
我们首先来看看用纯Python是如何实现__slots__
(为了将以下实现的slots与原slots区分开来,代码中用单下划线的_slots_
来代替)
class Member(object):
# 定义描述器实现slots属性的查找
def __init__(self, i):
self.i = i
def __get__(self, obj, type=None):
return obj._slotvalues[self.i]
def __set__(self, obj, value):
obj._slotvalues[self.i] = value
class Type(type):
# 使用元类实现slots
def __new__(self, name, bases, namespace):
slots = namespace.get('_slots_')
if slots:
for i, slot in enumerate(slots):
namespace[slot] = Member(i)
original_init = namespace.get('__init__')
def __init__(self, *args, **kwargs):
# 创建_slotvalues列表和调用原来的__init__
self._slotvalues = [None] * len(slots)
if original_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
namespace['__init__'] = __init__
return type.__new__(self, name, bases, namespace)
# Python2与Python3使用元类的区别
try:
class Object(object): __metaclass__ = Type
except:
class Object(metaclass=Type): pass
class A(Object):
_slots_ = 'x', 'y'
a = A()
a.x = 10
print(a.x)
在CPython中,当一个A类定义了__slots__ = ('x', 'y')
,A.x
就是一个有__get__
和__set__
方法的member_descriptor
,并且在每个实例中可以通过直接访问内存(direct memory access)获得。(具体实现是用偏移地址来记录描述器,通过公式可以直接计算出其在内存中的实际地址 ,访问__dict__
也是用相同的方法,也就是说访问A.__dict__
和A.x
描述器的速度是相近的)
在上面的例子中,我们用纯Python实现了一个等价的slots。当一个元类看到_slots_
定义了x和y,它会创建两个的类变量,x = Member(0)
和y = Member(1)
。然后,装饰__init__
方法让新的实例创建一个_slotvalues
列表。
例子中的实现和CPython不同的是:
例子中
_slotvalues
是一个存储在类对象外部的列表,而在CPython中它与实例对象存储在一起,可以通过直接访问内存获得。相应地,member decriptor
也不是存在外部列表中,而同样可以通过直接访问内存获得。默认情况下,
__new__
方法会为每个实例创建一个字典__dict__
来存储实例的属性。但如果定义了__slots__
,__new__
方法就不会再创建这个字典。由于不存在
__dict__
来存储新的属性,所以使用一个不在__slots__
中的属性时,程序会报错。
>>> class A(object): __slots__ = ('x')
>>> a = A()
>>> a.y = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Attribute: 'A' object has no attribute 'y'
可以利用这种特性来限制实例的属性。
更快的属性访问速度
默认情况下,访问一个实例的属性是通过访问该实例的__dict__
来实现的。如访问a.x
就相当于访问a.__dict__['x']
。为了便于理解,我粗略地将它拆分为四步:
a.x
2.a.__dict__
3.a.__dict__['x']
4. 结果
从__slots__
的实现可以得知,定义了__slots__
的类会为每个属性创建一个描述器。访问属性时就直接调用这个描述器。在这里我将它拆分为三步:
b.x
2.member decriptor
3. 结果
我在上文提到,访问__dict__
和描述器的速度是相近的,而通过__dict__
访问属性多了a.__dict__['x']
字典访值一步(一个哈希函数的消耗)。由此可以推断出,使用了__slots__
的类的属性访问速度比没有使用的要快。下面用一个例子验证:
from timeit import repeat
class A(object): pass
class B(object): __slots__ = ('x')
def get_set_del_fn(obj):
def get_set_del():
obj.x = 1
obj.x
del obj.x
return get_set_del
a = A()
b = B()
ta = min(repeat(get_set_del_fn(a)))
tb = min(repeat(get_set_del_fn(b)))
print("%.2f%%" % ((ta/tb - 1)*100))
在本人电脑上测试速度有0-20%左右的提升。
减少内存消耗
Python内置的字典本质是一个哈希表,它是一种用空间换时间的数据结构。为了解决冲突的问题,当字典使用量超过2/3时,Python会根据情况进行2-4倍的扩容。由此可预见,取消__dict__
的使用可以大幅减少实例的空间消耗。
下面用pympler
模块测试在不同属性数目下,使用__slots__
前后单个实例占用内存大小:
from string import ascii_letters
from pympler.asizeof import asizesof
def slots_memory(num=0):
attrs = list(ascii_letters[:num])
class Unslotted(object): pass
class Slotted(object): __slots__ = attrs
unslotted = Unslotted()
slotted = Slotter()
for attr in attrs:
unslotted.__dict__[attr] = 0
exec('slotted.%s = 0' % attr, globals(), locals())
memory_use = asizesof(slotted, unslotted, unslotted.__dict__)
return memory_use
def slots_test(nums):
return [slots_memory(num) for num in nums]
测试结果:(单位:字节)
属性数量 | slotted | unslotted(__dict__ ) |
---|---|---|
0 | 80 | 334(280) |
1 | 152 | 408(344) |
2 | 168 | 448(384) |
8 | 264 | 1456(1392) |
16 | 392 | 1776(1712) |
25 | 536 | 4440(4376) |
从上述结果可看到使用__slots__
能极大地减少内存空间的消耗,这也是最常见到的用法。
使用笔记
1. 只有非字符串的迭代器可以赋值给__slots__
>>> class A(object): __slots__ = ('a', 'b', 'c')
>>> class B(object): __slots__ = 'abcd'
>>> B.__slots__
'abc'
若直接将字符串赋值给它,就只有一个属性。
2. 关于slots的继承问题
在一般情况下,使用slots的类需要直接继承object
,如class Foo(object): __slots__ = ()
在继承自己创建的类时,我根据子类父类是否定义了__slots__
,将它细分为六种情况:
- 父类有,子类没有:
子类的实例还是会自动创建__dict__
来存储属性,不过父类__slots__
已有的属性不受影响。
>>> class Father(object): __slots__ = ('x')
>>> class Son(Base): pass
>>> son = Son()
>>> son.x, son.y = 1, 1
>>> son.__dict__
>>> {'y': 1}
- 父类没有,子类有:
虽然子类取消了__dict__
,但继承父类后它会继续生成。同上面一样,__slots__
已有的属性不受影响。
>>> class Father(object): pass
>>> class Son(Father): __slots__ = ('x')
>>> son = Son()
>>> son.x, son.y = 1, 1
>>> son.__dict__
>>> {'y': 1}
- 父类有,子类有:
只有子类的__slots__
有效,访问父类有子类没有的属性依然会报错。
>>> class Father(object): __slots__ = ('x', 'y')
>>> class Son(Father): __slots__ = ('x', 'z')
>>> son = Son()
>>> son.x, son.y, son.z = 1, 1, 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Son' object has no attribute 'y'
- 多个拥有非空slots的父类:
由于__slots__
的实现不是简单的列表或字典,多个父类的非空__slots__
不能直接合并,所以使用时会报错(即使多个父类的非空__slots__
是相同的)。
>>> class Father(object): __slots__ = ('x')
>>> class Mother(object): __slots__ = ('x')
>>> class Son(Father, Mother): pass
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
multiple bases have instance lay-out conflict
多个空slots的父类:
这是关于slots使用多继承唯一办法。某些父类有,某些父类没有:
跟第一种情况类似。
小结:为了正确使用__slots__
,最好直接继承object
。如有需要用到其他父类,则父类和子类都要定义slots,还要记得子类的slots会覆盖父类的slots。
除非所有父类的slots都为空,否则不要使用多继承。
3. 添加__dict__
获取动态特性
在特殊情况下,可以在__slots__
里添加__dict__
来获取与普通实例同样的动态特性。
>>> class A(object): __slots__ = ()
>>> class B(A): __slots__ = ('__dict__', 'x')
>>> b = B()
>>> b.x, b.y = 1, 1
>>> b.__dict__
{'y': 1}
4. 添加__weakref__
获取弱引用功能
__slots__
的实现不仅取消了__dict__
的生成,也取消了__weakref__
的生成。同样的,在__slots__
将其添加可以重新获取弱引用这一功能。
5. 不能通过类属性给实例设定默认值
定义了__slots__
后,这个类的类属性都变为了描述器。如果给类属性赋值,就会把描述器给覆盖了。
6. namedtuple
利用内置的namedtuple不可变的特性,结合slots,能创建出一个轻量不可变的实例。(约等于一个元组的大小)
>>> from collections import namedtuple
>>> class MyNt(namedtupele('MyNt', 'bar baz')): __slots__ = ()
>>> nt = MyNt('r', 'z')
>>> nt.bar
'r'
>>> nt.baz
'z'
总结
当一个类需要创建大量实例时,可以使用__slots__
来减少内存消耗。如果对访问属性的速度有要求,也可以酌情使用。另外可以利用slots的特性来限制实例的属性。而用在普通类身上时,使用__slots__
后会丧失动态添加属性和弱引用的功能,进而引起其他错误,所以在一般情况下不要使用它。
参考资料:
Python__slots__详解的更多相关文章
- (转)Python__slots__详解
原文:https://www.cnblogs.com/rainfd/p/slots.html#top 摘要 当一个类需要创建大量实例时,可以通过__slots__声明实例所需要的属性, 例如,clas ...
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
- EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解
前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- Android Notification 详解(一)——基本操作
Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...
- Android Notification 详解——基本操作
Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...
- Git初探--笔记整理和Git命令详解
几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...
- Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)
Android XML shape 标签使用详解 一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...
随机推荐
- [编织消息框架][rpc]使用篇
rpc 分两部份,一个是调用者,另一方是服务提供者 调用者只关心那个服务,传相应参数,返回内容就可以 而提供者根据调用相应服务同参数,工作处理后响应内容即可 根据他们的关系可以用JAVA 接口同实现类 ...
- windows 2003装.net 4.0时提示 WIC windows Imaging Component
运行此安装程序之前,必须安装32位windows映像处理组件(WIC) WIC windows Imaging Component下载地址: http://download.microsoft.com ...
- 每天一个linux命令(33)--df命令
周末是学习的好时光. 每天一个Linux命令,接下来是磁盘存储相关的,df, du命令 Linux中 df 命令的功能是用来检查Linux 服务器的文件系统的磁盘空间占用情况.可以利用该命令来获取硬 ...
- 撸基础篇系列,JAVA的NIO部分
前言:撸基础篇系列,避免每次都要从头开始看,写个自己的知识体系树 NIO 核心就是异步, 比如,复制文件,让操作系统去处理,等通知 BIO核心类 一,BIO NIO基本操作类 Bytebuffer 构 ...
- 阿里云开发之OSS数据迁移
最近由于项目需求,需要将一个aliyun账号下的oss数据导入到两一个aliyun账号下的oss,经过一番坎坷,最终搞定. 1.查看oss数据迁移官方文档,我是在本地windows电脑上进行操作的,先 ...
- 转 jquery怎么在header中设置请求信息
jquery是js的类库,js本身不能操作header,因为js是在浏览器加载页面过程中才开始执行的 header需要服务器端执行操作 如果是ajax,是可以设置header $.ajax({ url ...
- javascript作用域和闭包之我见
javascript作用域和闭包之我见 看了<你不知道的JavaScript(上卷)>的第一部分--作用域和闭包,感受颇深,遂写一篇读书笔记加深印象.路过的大牛欢迎指点,对这方面不懂的同学 ...
- Host文件修改后无效的解决办法
什么是hosts文件? 简单的说,hosts文件是用于本地dns服务(相关主题:什么是DNS缓存,如何清除DNS缓存?)的,采用ip 域名的格式写在一个文本文件当中,Hosts是一个没有扩展名的系统文 ...
- Java使用Schema模式对XML验证
XML允许创作者定义自己的标签,因其灵活的特性让其难以编写和解析.因此必须使用某种模式来约束其结构.目前最流行的这种模式有两种:DTD和SCHEMA,而后者以其独特的优势即将取代DTD模式,目前只是过 ...
- Java虚拟机(JVM)
Java虚拟机 Java字节码通过类加载器(Class Loader)为程序的执行加载所需要的全部类.在类的加载过程中,由于是按照先加载启动类库.再加载扩展类库最后加载用户自定义类库的顺序,从而避免一 ...