• (转)Python__slots__详解


    原文: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]

    测试结果:(单位:字节)

    属性数量slottedunslotted(__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

  • 相关阅读:
    307. Range Sum Query
    OLI 课程 & Java入学考试的五道题
    745. Prefix and Suffix Search 查找最大index的单词
    38.Count and Say 报数
    721. Accounts Merge合并电子邮件账户
    265. Paint House II 房子涂色K种选择的版本
    【转】如何做人性化的代码审查?从高到低、用例子
    java之struts2之文件下载
    java之struts2之文件上传
    java之struts2之拦截器
  • 原文地址:https://www.cnblogs.com/liujiacai/p/10562049.html
Copyright © 2020-2023  润新知