原文:https://www.cnblogs.com/rainfd/p/slots.html#top

摘要

当一个类需要创建大量实例时,可以通过__slots__声明实例所需要的属性,

例如,class Foo(object): __slots__ = ['foo']。这样做带来以下优点:

  1. 更快的属性访问速度
  2. 减少内存消耗

以下测试环境为Ubuntu16.04 Python2.7


Slots的实现

我们首先来看看用纯Python是如何实现__slots__(为了将以下实现的slots与原slots区分开来,代码中用单下划线的_slots_来代替)

  1. class Member(object):
  2. # 定义描述器实现slots属性的查找
  3. def __init__(self, i):
  4. self.i = i
  5. def __get__(self, obj, type=None):
  6. return obj._slotvalues[self.i]
  7. def __set__(self, obj, value):
  8. obj._slotvalues[self.i] = value
  9. class Type(type):
  10. # 使用元类实现slots
  11. def __new__(self, name, bases, namespace):
  12. slots = namespace.get('_slots_')
  13. if slots:
  14. for i, slot in enumerate(slots):
  15. namespace[slot] = Member(i)
  16. original_init = namespace.get('__init__')
  17. def __init__(self, *args, **kwargs):
  18. # 创建_slotvalues列表和调用原来的__init__
  19. self._slotvalues = [None] * len(slots)
  20. if original_init(self, *args, **kwargs):
  21. original_init(self, *args, **kwargs)
  22. namespace['__init__'] = __init__
  23. return type.__new__(self, name, bases, namespace)
  24. # Python2与Python3使用元类的区别
  25. try:
  26. class Object(object): __metaclass__ = Type
  27. except:
  28. class Object(metaclass=Type): pass
  29. class A(Object):
  30. _slots_ = 'x', 'y'
  31. a = A()
  32. a.x = 10
  33. 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__中的属性时,程序会报错。

  1. >>> class A(object): __slots__ = ('x')
  2. >>> a = A()
  3. >>> a.y = 1
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. Attribute: 'A' object has no attribute 'y'

可以利用这种特性来限制实例的属性。


更快的属性访问速度

默认情况下,访问一个实例的属性是通过访问该实例的__dict__来实现的。如访问a.x就相当于访问a.__dict__['x']。为了便于理解,我粗略地将它拆分为四步:

  1. a.x 2. a.__dict__ 3. a.__dict__['x'] 4. 结果

__slots__的实现可以得知,定义了__slots__的类会为每个属性创建一个描述器。访问属性时就直接调用这个描述器。在这里我将它拆分为三步:

  1. b.x 2. member decriptor 3. 结果

我在上文提到,访问__dict__和描述器的速度是相近的,而通过__dict__访问属性多了a.__dict__['x']字典访值一步(一个哈希函数的消耗)。由此可以推断出,使用了__slots__的类的属性访问速度比没有使用的要快。下面用一个例子验证:

  1. from timeit import repeat
  2. class A(object): pass
  3. class B(object): __slots__ = ('x')
  4. def get_set_del_fn(obj):
  5. def get_set_del():
  6. obj.x = 1
  7. obj.x
  8. del obj.x
  9. return get_set_del
  10. a = A()
  11. b = B()
  12. ta = min(repeat(get_set_del_fn(a)))
  13. tb = min(repeat(get_set_del_fn(b)))
  14. print("%.2f%%" % ((ta/tb - 1)*100))

在本人电脑上测试速度有0-20%左右的提升。


减少内存消耗

Python内置的字典本质是一个哈希表,它是一种用空间换时间的数据结构。为了解决冲突的问题,当字典使用量超过2/3时,Python会根据情况进行2-4倍的扩容。由此可预见,取消__dict__的使用可以大幅减少实例的空间消耗。

下面用pympler模块测试在不同属性数目下,使用__slots__前后单个实例占用内存大小:

  1. from string import ascii_letters
  2. from pympler.asizeof import asizesof
  3. def slots_memory(num=0):
  4. attrs = list(ascii_letters[:num])
  5. class Unslotted(object): pass
  6. class Slotted(object): __slots__ = attrs
  7. unslotted = Unslotted()
  8. slotted = Slotter()
  9. for attr in attrs:
  10. unslotted.__dict__[attr] = 0
  11. exec('slotted.%s = 0' % attr, globals(), locals())
  12. memory_use = asizesof(slotted, unslotted, unslotted.__dict__)
  13. return memory_use
  14. def slots_test(nums):
  15. 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__

  1. >>> class A(object): __slots__ = ('a', 'b', 'c')
  2. >>> class B(object): __slots__ = 'abcd'
  3. >>> B.__slots__
  4. 'abc'

若直接将字符串赋值给它,就只有一个属性。

2. 关于slots的继承问题

在一般情况下,使用slots的类需要直接继承object,如class Foo(object): __slots__ = ()

在继承自己创建的类时,我根据子类父类是否定义了__slots__,将它细分为六种情况:

  1. 父类有,子类没有:
    子类的实例还是会自动创建__dict__来存储属性,不过父类__slots__已有的属性不受影响。
  1. >>> class Father(object): __slots__ = ('x')
  2. >>> class Son(Base): pass
  3. >>> son = Son()
  4. >>> son.x, son.y = 1, 1
  5. >>> son.__dict__
  6. >>> {'y': 1}
  1. 父类没有,子类有:
    虽然子类取消了__dict__,但继承父类后它会继续生成。同上面一样,__slots__已有的属性不受影响。
  1. >>> class Father(object): pass
  2. >>> class Son(Father): __slots__ = ('x')
  3. >>> son = Son()
  4. >>> son.x, son.y = 1, 1
  5. >>> son.__dict__
  6. >>> {'y': 1}
  1. 父类有,子类有:
    只有子类的__slots__有效,访问父类有子类没有的属性依然会报错。
  1. >>> class Father(object): __slots__ = ('x', 'y')
  2. >>> class Son(Father): __slots__ = ('x', 'z')
  3. >>> son = Son()
  4. >>> son.x, son.y, son.z = 1, 1, 1
  5. Traceback (most recent call last):
  6. File "<stdin>", line 1, in <module>
  7. AttributeError: 'Son' object has no attribute 'y'
  1. 多个拥有非空slots的父类:
    由于__slots__的实现不是简单的列表或字典,多个父类的非空__slots__不能直接合并,所以使用时会报错(即使多个父类的非空__slots__是相同的)。
  1. >>> class Father(object): __slots__ = ('x')
  2. >>> class Mother(object): __slots__ = ('x')
  3. >>> class Son(Father, Mother): pass
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. TypeError: Error when calling the metaclass bases
  7. multiple bases have instance lay-out conflict
  1. 多个空slots的父类:
    这是关于slots使用多继承唯一办法。

  2. 某些父类有,某些父类没有:
    跟第一种情况类似。

小结:为了正确使用__slots__,最好直接继承object。如有需要用到其他父类,则父类和子类都要定义slots,还要记得子类的slots会覆盖父类的slots。
除非所有父类的slots都为空,否则不要使用多继承。

3. 添加__dict__获取动态特性

在特殊情况下,可以在__slots__里添加__dict__来获取与普通实例同样的动态特性。

  1. >>> class A(object): __slots__ = ()
  2. >>> class B(A): __slots__ = ('__dict__', 'x')
  3. >>> b = B()
  4. >>> b.x, b.y = 1, 1
  5. >>> b.__dict__
  6. {'y': 1}

4. 添加__weakref__获取弱引用功能

__slots__的实现不仅取消了__dict__的生成,也取消了__weakref__的生成。同样的,在__slots__将其添加可以重新获取弱引用这一功能。

5. 不能通过类属性给实例设定默认值

定义了__slots__后,这个类的类属性都变为了描述器。如果给类属性赋值,就会把描述器给覆盖了。

6. namedtuple

利用内置的namedtuple不可变的特性,结合slots,能创建出一个轻量不可变的实例。(约等于一个元组的大小)

  1. >>> from collections import namedtuple
  2. >>> class MyNt(namedtupele('MyNt', 'bar baz')): __slots__ = ()
  3. >>> nt = MyNt('r', 'z')
  4. >>> nt.bar
  5. 'r'
  6. >>> nt.baz
  7. 'z'

总结

当一个类需要创建大量实例时,可以使用__slots__来减少内存消耗。如果对访问属性的速度有要求,也可以酌情使用。另外可以利用slots的特性来限制实例的属性。而用在普通类身上时,使用__slots__后会丧失动态添加属性和弱引用的功能,进而引起其他错误,所以在一般情况下不要使用它。

参考资料

Usage of slots?

How slots are implemented

(转)Python__slots__详解的更多相关文章

  1. Python__slots__详解

    摘要 当一个类需要创建大量实例时,可以通过__slots__声明实例所需要的属性, 例如,class Foo(object): __slots__ = ['foo'].这样做带来以下优点: 更快的属性 ...

  2. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  3. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  4. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  5. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  6. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  7. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  8. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...

  9. Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

    Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...

随机推荐

  1. JavaScript基础函数---李老师的

    <!DOCTYPE html><html><head>    <title>demo2html</title>    <meta ch ...

  2. python数据结构(二)------列表

    本文将重点梳理列表及列表操作. 2.1 list函数 2.2 基本列表操作 2.3 列表方法 2.1 list函数 >>>list('hello') ['h','e','l',l', ...

  3. mac ssh,mac xshell,xshell替代,ssh客户端,ssh工具,远程桌面加速

    下载地址 Windows版下载地址:http://www.hostbuf.com/downloads/finalshell_install.exe Mac版,Linux版安装及教程:http://ww ...

  4. node-sass 不能正常安装解决办法

    web前端在安装node包时,总是报错,究其原因是node-sass没有被正常安装. 根本原因是国内网络的原因. 最终的解决方法是通过淘宝的npm镜像安装node-sass 首先安装cnpm npm ...

  5. Python从入门到精通之Seventh!

    函数浅析:可以减少代码重用,保持一致性,可扩展性,易维护性. 定义方法:def 函数名(形参):     '''功能注释'''      代码块 打印函数名时,会出现函数的内存地址.两个函数名相同时, ...

  6. vue中引入vux

    1.安装相关依赖 cnpm install vux --save cnpm install vux-loader --save-dev cnpm install less less-loader -- ...

  7. CentOS 6下升级Python版本

    CentOS6.8默认的python版本是2.6,而现在好多python组件开始只支持2.7以上的版本,比如说我今天遇到的pip install pysqlite,升级python版本是一个痛苦但又常 ...

  8. latex_引用参考文献格式,引用多篇参考文献

    以下内容在TeXstudio中实现: LaTeX 标准选项及其样式命令为: \bibliographystyle{type} 共有以下8种: plain,按字母的顺序排列,比较次序为作者.年度和标题. ...

  9. HelloWorld带我入门JAVA(一)

    基本环境配置可以百度完成,给个比较全面的网址http://c.biancheng.net/java/10/ 创建第一个java工程 通过Eclipse运行程序 启动Eclipse,在菜单中选择“文件 ...

  10. kubernetes CSI 插件机制学习笔记

    前言 最近在极客时间订阅了kubernetes的专栏,这篇文章是想记录一下自己学习 CSI 插件机制 (container-storage-interface) 的过程,加深一下记忆. 准备工作 老师 ...