用描述器 (Descriptor) 實作 Python 物件以節省記憶體資源
參考資料:
Dictionary Attribute
一般的自訂 Class 如下:
class A:
pass
a = A()
a.c = 1
print(a.c) #1
其實同於:
a = A() a.__dict__['c'] = 1 print(a.__dict__['c'])
其中 a.c 和 a.__dict__['c'] 的記憶體位置是一樣的。
是因為透過存取 __dict__ 特性的方式,用字典類型將鍵值存在 a 物件中。
這樣子可以透過外部疊加特性 (Attribute) 至 a 物件,甚至是 function 都可以在建立 a 物件後加上,不過只有 a 物件可以使用。
Slots Attribute
Python 提供另一種方式存取 Class 的特性:
class A:
__slots__ = ('b', 'c')
a = A()
a.b = 1 #Okay
a.c = 2 #Okay
print(a.c) #2
a.d = 3
'''
AttributeError: 'A' object has no attribute 'd'
'''
使用 __slots__特性時,會移除 __dict__ 特性,改用描述器 (Descriptor) 來存取其他特性。
上述程式碼同下:
a = A() A.__dict__['b'].__set__(a, 1) A.__dict__['c'].__set__(a, 2) print(A.__dict__['c'].__get__(a, A))
而 A.__dict__ 是屬於 mappingproxy 類型,並非一般的 dict 類型,因此無法動態加入新的成員,因此 a 物件就無法加入新特性。
>>> A.__dict__
mappingproxy({'__module__': '__main__', 'b': <member 'b' of 'A' objects>, '__doc__': None, 'c': <member 'c' of 'A' objects>, '__slots__': ('b', 'c')})
>>> a.__dict__
Traceback (innermost last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__dict__'
使用 Descriptor 來操作類型的特性時,是使用固定記憶體位置,會比 dict 還節省資源。
因此若是只有少數 attribute 物件時,使用 __slots__ 列舉,可以提昇較多效能。
這種方法類似 C# 的結構 (Structure) 比類別 (Class) 省資源的原理。
繼承
在繼承類型時,要注意幾個問題:
- 沒有定義
__slots__時,會自動產生__dict__特性。 - 當
__dict__和__slots__特性共存時,就會失去__slots__的意義了。
由於子類型會接收父類型的特性,因此下面的情況都不盡理想:
#這樣會導致 B 類型無法使用 A 類型的特性。
class A:
__slots__ = ('a',)
class B(A):
__slots__ = ('b',)
#這樣會導致 B 類型產生 __dict__。
class A:
__slots__ = ('a',)
class B(A):
pass
#這樣會導致 B 類型繼承 A 類型的 __dict__。
class A:
pass
class B(A):
__slots__ = ('b',)
可以使用這樣的方法解決:
class A:
__slots__ = ('a',)
class B(A):
__slots__ = A.__slots__+('b',)
b = B()
b.a = 10
print(b.a) #10
PyQt 的類型由於是 wrapper 製作的,已經有空的 __dict__ 特性,繼承後使用此方法就沒意義了。
Comments
comments powered by Disqus