槽(slots)可以使用__slots_屬性來為自定的類設置以一個靜態屬性列表,並在類的每個實例中跳過__dict__字典的創建過程,可以提高訪問速度,節省內存消耗
由於Python是動態語言,任何實例在運行期都可以動態地添加屬性。
如果要限制添加的屬性,例如,Student類只允許添加 name、gender和score 這3個屬性,就可以利用Python的一個特殊的__slots__來實現
class Student(object): __slots__ = ('name', 'gender', 'score') def __init__(self, name, gender, score): self.name = name self.gender = gender self.score = score
創建實例,並向其中新增屬性
>>> s = Student('Bob', 'male', 59) >>> s.name = 'Tim' # OK >>> s.score = 99 # OK >>> s.grade = 'A'
新增屬性grade執行后,拋異常【AttributeError: 'student' object has no attribute 'gride'】
當一個Student類定義了__slots__ = ('name', 'age')
,Studen.name
就是一個有__get__
和__set__
方法的member_descriptor
__slots__的目的是限制當前類所能擁有的屬性,如果不需要添加任意動態的屬性,使用__slots__也能節省內存。
__slots__兩大特性
更快的屬性訪問速度
默認情況下,訪問一個實例的屬性是通過訪問該實例的__dict__
來實現的。如訪問s.name就相當於訪問s.__dict__['name']
。為了便於理解,我粗略地將它拆分為四步:s.name
>> s.__dict__
>>. s.__dict__['name']
>>. 結果
從__slots__
的實現可以得知,定義了__slots__
的類會為每個屬性創建一個描述器。訪問屬性時就直接調用這個描述器。在這里我將它拆分為三步:b.x
>>. member decriptor
>>. 結果
我在上文提到,訪問__dict__
和描述器的速度是相近的,而通過__dict__
訪問屬性多了s.__dict__['name']
字典訪值一步(一個哈希函數的消耗)。由此可以推斷出,使用了__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-25%左右的提升。
減少內存消耗
python內置的字典本質是一個哈希表,它是一種用空間換時間的數據結構。為了解決沖突的問題,當字典使用量超過2/3時,Python會根據情況進行2-4倍的擴容。由此可預見,取消__dict__
的使用可以大幅減少實例的空間消耗。
關於slots的繼承問題
在一般情況下,使用slots的類需要直接繼承object
,如class Foo(object): __slots__ = ()
在繼承自己創建的類時,我根據子類父類是否定義了__slots__
,將它細分為三種情況:(暫未列出多父類的情況)
- 父類有,子類沒有:子類的實例還是會自動創建
__dict__
來存儲屬性,不過父類__slots__
已有的屬性不受影響。 - 父類沒有,子類有:雖然子類取消了
__dict__
,但繼承父類后它會繼續生成。同上面一樣,__slots__
已有的屬性不受影響。 - 父類有,子類有:只有子類的
__slots__
有效,訪問父類有子類沒有的屬性依然會報錯
因此:為了正確使用__slots__
,最好直接繼承object
。如有需要用到其他父類,則父類和子類都要定義slots,還要記得子類的slots會覆蓋父類的slots。
除非所有父類的slots都為空,否則不要使用多繼承。
在特殊情況下,可以在__slots__
里添加__dict__
來獲取與普通實例同樣的動態特性。
當一個類需要創建大量實例時,可以使用__slots__
來減少內存消耗。如果對訪問屬性的速度有要求,也可以酌情使用。另外可以利用slots的特性來限制實例的屬性。而用在普通類身上時,使用__slots__
后會喪失動態添加屬性和弱引用的功能,進而引起其他錯誤,所以在一般情況下不要使用它。