文章目录
1 功能
__slots__是python类的魔法属性,可接收一个iterable对象作为属性。定义后,该类实例只能创建__slots__中声明的属性,否则报错。
class Test(object):
__slots__ = ['a']
if __name__ == '__main__':
t = Test()
t.a = 1
Test.c = 3 # 类属性仍然可以自由添加
print(t.c) # 输出:3
t.b = 2 # AttributeError: 'Test' object has no attribute 'b'
从上面的例子能看出__slots__的具体功能,它的作用就是用来约束类实例的属性,不允许类实例调用方向实例随意添加属性。
2 优点
2.1 节省内存
python的类在没有定义__slots__时,实例的属性管理其实依赖字典, 也就是魔法属性__dict__,它其实就是个存放实例所有属性及对应值的字典。需要注意的是,定义了__slots__的类实例不再拥有__dict__属性。
在python中字典的内存分配规则是,先预分配一块内存区,当元素添加到一定阈值时进行扩容再分配一块比较大的内存区,由此可见__dict__存储属性会预留比较大的空间,因此会存在比较大的内存浪费。
__slots__的做法就是在创建实例之初就按照__slots__中声明的属性分配定长内存,实际就是定长列表,因此会更加节省内存。
实例说明:
from memory_profiler import profile
class TestA(object):
__slots__ = ['a', 'b', 'c']
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
class TestB(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
@profile
def test():
temp = [TestA(i, i+1, i+2) for i in range(10000)]
del temp
temp = [TestB(i, i+1, i+2) for i in range(10000)]
del temp
if __name__ == '__main__':
test()
可以看到定义了__slots__的类实例会更加节省内存。
总结一下实际使用中应该使用__slots__的场景:需要大量创建固定属性的实例时。
2.2 更快的属性访问速度
使用__slots__访问属性,实际节省了一次哈希的过程,因此属性访问速度会更快一些。
实例说明:
from line_profiler import LineProfiler
slots = ['a{}'.format(i) for i in range(10000)]
class TestA(object):
__slots__ = slots
def __init__(self):
for i in slots:
self.__setattr__(i, i[1:])
class TestB(object):
def __init__(self):
for i in slots:
self.__setattr__(i, i[1:])
def test():
a = TestA()
b = TestB()
for i in range(10000):
tmp = a.a6666
for i in range(10000):
tmp = b.a6666
if __name__ == '__main__':
profiler = LineProfiler()
profiler(test)()
profiler.print_stats()
3 原理
理解原理的最好方法当然是阅读源码,这里本人就不献丑了,找到一篇大佬的博客对于__slots__源码讲解的还蛮清晰,真正理解了原理,很多关于__slots__的问题也就迎刃而解了。
这里给出一个python版的__slots__实现,用以说明原理:
'Rough approximation of how slots work'
class Member(object):
'Descriptor implementing slot lookup'
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):
'Metaclass that detects and implements _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, **kwds):
'Create _slotvalues list and call the original __init__'
self._slotvalues = [None] * len(slots)
if original_init is not None:
original_init(self, *args, **kwds)
namespace['__init__'] = __init__
return type.__new__(self, name, bases, namespace)
从python实现中,我们可以看到,定义__slots__之后,类会为__slots__中每个属性创建一个Member
实例用来记录属性对应值在_slotvalues
中的偏移量,类实例会创建一个定长列表_slotvalues
用来存储对应的属性值。因为使用定长列表和偏移量所以也就更省内存和访问时间。
4 使用注意点
4.1 赋值
__slots__接收可迭代对象赋值,一般使用list或者tuple即可。注意:
- dict赋值只会取keys();
- str赋值只会有一个属性就是赋值的字符串。
从源码中可以知道,__slots__中声明的属性可以重复(然而除浪费空间之外没什么卵用)除了__dict__和__weakref__之外(是的,__slots__之中还可以包含这两个属性)。
4.2 向__slots__中添加__dict__
在__slots__中添加__dict__之后可以恢复动态添加属性功能,然而既生瑜何生亮,从来没这么用过。
class Test(object):
__slots__ = ['a', '__dict__']
if __name__ == '__main__':
t = Test()
t.a = 'a'
t.b = 'b'
print(t.__dict__) # 输出:{'b': 'b'}
4.3 类属性赋值的限制
从__slots__原理的介绍中,我们知道__slots__之中声明的类属性其实是member_descriptor实例,类实例在取值时其实时对于实例属性_slotsvalue
做列表寻址。因此如果对类属性重新赋值,会破坏寻址过程,影响实例属性取值。
实例说明:
class Test1(object):
__slots__ = ['a']
class Test2(object):
pass
if __name__ == '__main__':
x = Test1()
x.a = 'a'
Test1.a = 1
print(x.a) # 输出:1
y = Test2()
y.a = 'a'
Test2.a = 1
print(y.a) # 输出:a
4.4 __slots__在继承中的问题
在继承中使用__slots__是比较混乱的,情况根据父类子类有没有定义__slots__分为以下几种情况:
4.4.1 父类有,子类无
子类实例继承父类__slots__中的属性,同时也会自动创建__dict__用来动态拓展属性。
class Parent(object):
__slots__ = ['x']
class Child(Parent):
pass
c = Child()
c.x, c.y = 1, 2
print(c.__slots__) # 输出:['x']
print(c.__dict__) # 输出:{'y': 2}
4.4.2 父类无,子类有
子类继承父类__dict__可动态拓展属性,自身__slots__中属性不变。
class Parent(object):
pass
class Child(Parent):
__slots__ = ['x']
c = Child()
c.x, c.y = 1, 2
print(c.__slots__) # 输出:['x']
print(c.__dict__) # 输出:{y: 2}
4.4.3 父类有,子类有
子类__slots__会覆盖父类__slots__,子类仍可访问父类slots中有但自己没有的属性。
class Parent(object):
__slots__ = ['x']
class Child(Parent):
__slots__ = ['y']
c = Child()
print(c.__slots__) # 输出:['y']
c.x, c.y = 1, 2
print(c.x, c.y) # 输出:1 2
4.4.4 多父类继承
若只有一个父类有非空__slots__,其他父类无或__slots__为空,则情况同上面单继承类似;
若多个父类有非空__slots__,则会报错。
class Parent(object):
__slots__ = ['x']
class ParentA(object):
__slots__ = ['y']
class Child(Parent, ParentA): # 报错:TypeError: multiple bases have instance lay-out conflict
pass
推荐阅读: