Python深入理解__slots__

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即可。注意:

  1. dict赋值只会取keys();
  2. 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

推荐阅读:

  1. HOW __SLOTS__ ARE IMPLEMENTED
  2. python slots源码分析
  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin9436

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值