摘要

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

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

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

以下测试环境为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']。为了便于理解,我粗略地将它拆分为四步:

  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__的类的属性访问速度比没有使用的要快。下面用一个例子验证:

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__,将它细分为六种情况:

  1. 父类有,子类没有:

    子类的实例还是会自动创建__dict__来存储属性,不过父类__slots__已有的属性不受影响。
>>> class Father(object): __slots__ = ('x')
>>> class Son(Base): pass
>>> son = Son()
>>> son.x, son.y = 1, 1
>>> son.__dict__
>>> {'y': 1}
  1. 父类没有,子类有:

    虽然子类取消了__dict__,但继承父类后它会继续生成。同上面一样,__slots__已有的属性不受影响。
>>> class Father(object): pass
>>> class Son(Father): __slots__ = ('x')
>>> son = Son()
>>> son.x, son.y = 1, 1
>>> son.__dict__
>>> {'y': 1}
  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'
  1. 多个拥有非空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
  1. 多个空slots的父类:

    这是关于slots使用多继承唯一办法。

  2. 某些父类有,某些父类没有:

    跟第一种情况类似。

小结:为了正确使用__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__后会丧失动态添加属性和弱引用的功能,进而引起其他错误,所以在一般情况下不要使用它。

参考资料

Usage of slots?

How slots are implemented

Python__slots__详解的更多相关文章

  1. (转)Python__slots__详解

    原文:https://www.cnblogs.com/rainfd/p/slots.html#top 摘要 当一个类需要创建大量实例时,可以通过__slots__声明实例所需要的属性, 例如,clas ...

  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. hibernate中一种导致a different object with the same identifier value was already associated with the session错误方式及解决方法

    先将自己出现错误的全部代码都贴出来: hibernate.cfg.xml <?xml version="1.0" encoding="UTF-8"?> ...

  2. 前端发展态势 && 前端工作流程个人浅析

    于在未开启cleartype的情况下,一些中文字体在非偶数字号下的显示效果欠佳,所以一般建议使用12.14.16.18.22px等偶数字号.也就 是对某个分辨率选择离它最近的偶数字号.例如:屏幕横向分 ...

  3. CSS中padding和margin以及用法

    CSS中padding与margin 1.padding:内边距,表示控件内容相对于边缘的距离. 2.margin:外边距,表示控件边缘相对于父空间的边缘. 参考:http://www.studyof ...

  4. MySQL调优三步曲(慢查询、explain profile)

    在做性能测试中经常会遇到一些sql的问题,其实做性能测试这几年遇到问题最多还是数据库这块,要么就是IO高要么就是cpu高,所以对数据的优化在性能测试过程中占据着很重要的地方,下面我就介绍一些msyql ...

  5. Spring事务管理源码分析

    Spring事务管理方式 依据Spring.xsd文件可以发现,Spring提供了advice,annotation-driven,jta-transaction-manager3种事务管理方式.详情 ...

  6. 双系统win7和ubuntu14.04进入了grub rescue>

    可以跳过的废话:最近在学习caffe,需要在linux下安装cuda,sudo apt-get install cuda后,出现了由于根目录/空间不足而失败的情况. 于是想把win7下80G的一个盘格 ...

  7. 微信小程序怎样提高应用速度小技巧

    作者:vicyao, 腾讯web前端开发 高级工程师商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:http://wetest.qq.com/lab/view/294.htm ...

  8. 【解题报告】VijosP1448校门外的树(困难版)

    原题: 校门外有很多树,有苹果树,香蕉树,有会扔石头的,有可以吃掉补充体力的--如今学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两个操作:K=1,K=1,读入l.r ...

  9. jenkins配置邮箱服务器(126邮箱)

    安装Email Extension Plugin 安装过程容易失败,多试几次 一.开启126邮件的SMTP获取授权码 二.配置管理员邮件地址   三.设置邮件通知 四.点击Test Configura ...

  10. 阶乘运算——ACM

    大数阶乘 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 我们都知道如何计算一个数的阶乘,可是,如果这个数很大呢,我们该如何去计算它并输出它?   输入 输入一个整数 ...