用描述器 (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.ca.__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) 省資源的原理。

繼承

在繼承類型時,要注意幾個問題:

  1. 沒有定義 __slots__ 時,會自動產生 __dict__ 特性。
  2. __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