Python__slots__詳解

摘要html

當一個類須要建立大量實例時,能夠經過__slots__聲明實例所須要的屬性,python

例如,class Foo(object): __slots__ = ['foo']。這樣作帶來如下優勢:api

  1. 更快的屬性訪問速度
  2. 減小內存消耗

如下測試環境爲Ubuntu16.04 Python2.7markdown


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列表。post

例子中的實現和CPython不一樣的是:測試

  • 例子中_slotvalues是一個存儲在類對象外部的列表,而在CPython中它與實例對象存儲在一塊兒,能夠經過直接訪問內存得到。相應地,member decriptor也不是存在外部列表中,而一樣能夠經過直接訪問內存得到。spa

  • 默認狀況下,__new__方法會爲每一個實例建立一個字典__dict__來存儲實例的屬性。但若是定義了__slots____new__方法就不會再建立這個字典。code

  • 因爲不存在__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]

測試結果:(單位:字節)

屬性數量 slotted unslotted(__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

相關文章
相關標籤/搜索