摘要html
當一個類須要建立大量實例時,能夠經過__slots__
聲明實例所須要的屬性,python
例如,class Foo(object): __slots__ = ['foo']
。這樣作帶來如下優勢:api
- 更快的屬性訪問速度
- 減小內存消耗
如下測試環境爲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']
。爲了便於理解,我粗略地將它拆分爲四步:
a.x
2.a.__dict__
3.a.__dict__['x']
4. 結果
從__slots__
的實現能夠得知,定義了__slots__
的類會爲每一個屬性建立一個描述器。訪問屬性時就直接調用這個描述器。在這裏我將它拆分爲三步:
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__
,將它細分爲六種狀況:
- 父類有,子類沒有:
子類的實例仍是會自動建立__dict__
來存儲屬性,不過父類__slots__
已有的屬性不受影響。
>>> class Father(object): __slots__ = ('x') >>> class Son(Base): pass >>> son = Son() >>> son.x, son.y = 1, 1 >>> son.__dict__ >>> {'y': 1}
- 父類沒有,子類有:
雖然子類取消了__dict__
,但繼承父類後它會繼續生成。同上面同樣,__slots__
已有的屬性不受影響。
>>> class Father(object): pass >>> class Son(Father): __slots__ = ('x') >>> son = Son() >>> son.x, son.y = 1, 1 >>> son.__dict__ >>> {'y': 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'
- 多個擁有非空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
-
多個空slots的父類:
這是關於slots使用多繼承惟一辦法。 -
某些父類有,某些父類沒有:
跟第一種狀況相似。
小結:爲了正確使用__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__
後會喪失動態添加屬性和弱引用的功能,進而引發其餘錯誤,因此在通常狀況下不要使用它。
參考資料: