当一个类需要创建大量实例时,可以通过__slots__
声明实例所需要的属性,
例如,class Foo(object): __slots__ = ['foo', 'bar']
。这样做带来以下优点:
- 限制实例的属性
- 更快的属性访问速度
- 减少内存消耗
限制实例的属性:
class A:
__slots__ = ['x', 'y']
a = A()
a.x # 抛出异常,因为此时还没赋值x
a.x = 10 # 在__slots__中的属性,可以赋值
a.x # 在__slots__中的属性,如果赋值了,可以访问
a.z = 10 # 不在__slots__中的属性,不可以赋值,也不可以访问
为什么定义了__slots__之后,会限制其它属性的添加、访问?因为
默认情况下,__new__
方法会为每个实例创建一个字典__dict__
来存储实例的属性。
但如果定义了__slots__
,__new__
方法就不会再创建这个字典。
由于不存在__dict__
来存储新的属性,所以使用一个不在__slots__
中的属性时,程序会报错。
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Attribute: 'A' object has no attribute 'z'
更快的属性访问速度:
我们知道,默认情况下,类的成员都存储在__dict__字典中,如要访问属性x,就相当于访问a.__dict__['x']
。为了便于理解,我粗略地将它拆分为四步:
-
a.x
2.a.__dict__
3.a.__dict__['x']
4. 结果
如果一个类实现__slots__,就不会创建__dict__字典。
定义了__slots__
的类会为每个属性创建一个描述器。
如:当一个A类定义了__slots__ = ('x', 'y')
,A.x
就是一个有__get__
和__set__
方法的member_descriptor,并且在每个实例中可以通过直接访问内存(direct memory access)获得。(具体实现是用偏移地址来记录描述器,通过公式可以直接计算出其在内存中的实际地址 ,访问__dict__也是用相同的方法,也就是说访问A.__dict__和A.x描述器的速度是相近的)
访问属性时就直接调用这个描述器。在这里我将它拆分为三步:
-
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)) # 29.20%,可见使用__slots__访问属性,速度提高了20%
减少内存消耗
Python内置的字典本质是一个哈希表,它是一种用空间换时间的数据结构。为了解决冲突的问题,当字典使用量超过2/3时,Python会根据情况进行2-4倍的扩容。由此可预见,取消__dict__
的使用可以大幅减少实例的空间消耗。
关于slots的继承问题
在一般情况下,使用slots的类需要直接继承object
,如class Foo(object): __slots__ = ()
在继承自己创建的类时,我根据子类父类是否定义了__slots__
,将它细分为六种情况:
1.父类有,子类没有:
子类的实例还是会自动创建__dict__
来存储属性,不过父类__slots__
已有的属性不受影响。
class Father(object): __slots__ = ('x')
class Son(Father): pass
son = Son()
son.x, son.y = 1, 1
son.__dict__
# Out[17]: {'y': 1}
2.父类没有,子类有:
虽然子类取消了__dict__
,但继承父类后它会继续生成。同上面一样,__slots__
已有的属性不受影响。
class Father(object): pass
class Son(Father): __slots__ = ('x')
son = Son()
son.x, son.y = 1, 1
son.__dict__
# Out[20]: {'y': 1}
3.父类有,子类有:
父类与子类都没有__dict__,子类除了拥有自己的__slots__属性,也拥有父类的__slots__属性。
__slots__
的实现不仅取消了__dict__
的生成,也取消了__weakref__
的生成。同样的,在__slots__
将其添加可以重新获取弱引用这一功能。
添加__dict__
获取动态特性:
在特殊情况下,可以在__slots__
里添加__dict__
来获取与普通实例同样的动态特性。
class A: __slots = ("__dict__", "x")
a = A()
a.__dict__
# Out[9]: {}