原文: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_来代替)

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__详解

    摘要 当一个类需要创建大量实例时,可以通过__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. Docker安装nexus

    一.Nexus3安装 sudo docker search nexus sudo docker pull sonatype/nexus3 sudo docker images sudo docker ...

  2. spark原理

    SparkContext将应用程序代码分发到各Executors,最后将任务(Task)分配给executors执行 Application: Appliction都是指用户编写的Spark应用程序, ...

  3. NOIP刷题建议(未完结)

    1NOIP提高组真题 2NOI部分题目 为什么要写这个? 主要是一个人在硕大的机房里打(wan)代(you)码(xi),没多少时间了,所以打算来总结一下. 这个也是为我接下来冲刺做一个准备. 这个会不 ...

  4. 移动 web 适配

    一.移动 web 开发与适配 1.跑在手机端的 web 页面(H5 页面) 2.跨平台(PC 端.手机端 - 安卓.IOS) 3.基于 webview(终端开发技术的一个组件) 4.告别 IE 拥抱  ...

  5. windowsSevice程序和topshelf程序创建服务对比

    文章原地址:http://www.80iter.com/blog/1451523192435464 Topshelf 创建.net服务整理和安装步骤 windowsService和topshelf服务 ...

  6. [小结] 中山纪念中学2018暑期训练小结(划掉)(颓废记)-Day10

    [小结] 中山纪念中学2018暑期训练小结(划掉)(颓废记)-Day10 各位看众朋友们,你们好,今天是2018年08月14日,星期二,农历七月初四,欢迎阅看今天的颓废联编节目 最近发生的灵异事件有 ...

  7. vue的指令

    我之前学了学angular 发现angular和vue的指令有点类似 先说一下 new  Vue({          el: "#box", // element(元素) 当前作 ...

  8. U-Boot Makefile分析(2) config.mk分析

    浏览一下U-Boot各个子目录下的Makefile可以看到,几乎他们都会包含$(TOPDIR)/config.mk,那么这个文件进行了什么操作呢?简单概括:读入include/config.mk.in ...

  9. enumerate的用法

    names = ["tony","huluwa","tom","jerry","lala"]for ...

  10. linux 最为常用的命令

    系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 cat /proc/cpuinfo 显示CPU info的信息 ...