Published: 2016-11-10

Python类的 __slots__ 属性

Table of Contents

1 为什么有 __slots__ 属性?

默认情况下,python对象队象的每个实例(instance)都会有一个字典来存储该实例的属性,这样做的好处在于运行时期每个对象可以任意设置新的属性。而相对应的坏处是,当创建成百上千个这样的实例的时候回很浪费内存。所以引入 __slots__ ,用来指定实例只拥有固定的属性,因此python会给每个实例对象分配固定的内存空间,从而减少内存消耗。而且使用 __slots__ 可以加快属性的访问。

2 用法

__slots__ 可以被设置成属性名称的字符串,可遍历的对象或者序列。 之前在看odoo源码缓存相关的内容时,看到过下面这个例子:

class ormcache_counter(object):
    """ Statistic counters for cache entries. """
    __slots__ = ['hit', 'miss', 'err']

    def __init__(self):
        self.hit = 0
        self.miss = 0
        self.err = 0

    @property
    def ratio(self):
        return 100.0 * self.hit / (self.hit + self.miss or 1)

这里创建了一个用来记录每个方法缓存情况的对象,因为对于需要每个缓存的方法,都会创建一个该实例来记录缓存的状况(比如缓存用到或没用的次数等),所以为了节省内存加快访问速度这里指定了该对象拥有的三个属性。

3 测试

3.1 访问速度测试

timeit是python一个用来简单测试运行时间的模块,详细可参见官方文档

# In Python2.7

# test1.py
import timeit

class Foo(object): __slots__ = 'foo',

class Bar(object): pass

slotted = Foo()
not_slotted = Bar()

def get_set_delete_fn(obj):
    def get_set_delete():
        obj.foo = 'foo'
        obj.foo
        del obj.foo
    return get_set_delete


# In REPL
>>> from test1 import *
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.24305510520935059
>>> min(timeit.repeat(get_set_delete_fn(slotted)))
min(timeit.repeat(get_set_delete_fn(slotted)))
0.21287798881530762

可以看见,使用 __slots__ 的对象有更快的访问速度,虽然在python2.7中差别没有在python3中那么明显

3.2 内存占用参考

关于内存占用情况的测试我还没测,但可以参考 stackoverflow上的测试,我这里机(无)智(耻)地取个结果:

# 单位 bytes
attrs  __slots__    no slots declared + __dict__
none       16        64 (+ 280 if __dict__ referenced)
one        56        64 + 280
two        64        64 + 280
six        96        64 + 1048
22        224        64 + 3352

可以明显看到内存占用减少的情况。

4 注意事项

__dict__ 可以理解成类里面存储属性的字典,

  1. 当一个类A继承自一个没有定义 __slots__ 的类B时,A是有 __dict__ 属性,这是再定义 __slots__ 属性没有意义, 不能达到限制内存的作用
  2. 当尝试给一个定义了 __slots__ 的类,而没有定义 __dict__ 的类设置不在 __slots__ 指定的那些属性时,会导致一个"AttributeError"

其它注意请参照文档

Author: Nisen

Email: imnisen@163.com