使用__slots__ 類屬性節省空間
默認情況下,Python 在各個實例中名為__dict__ 的字典里存儲實例屬
性。如 3.9.3 節所述,為了使用底層的散列表提升訪問速度,字典會消
耗大量內存。如果要處理數百萬個屬性不多的實例,通過__slots__
類屬性,能節省大量內存,方法是讓解釋器在元組中存儲實例屬性,而
不用字典。
繼承自超類的__slots__ 屬性沒有效果。Python 只會使用
各個類中定義的__slots__ 屬性。
定義__slots__ 的方式是,創建一個類屬性,使用__slots__ 這個名
字,并把它的值設為一個字符串構成的可迭代對象,其中各個元素表示
各個實例屬性。我喜歡使用元組,因為這樣定義的__slots__ 中所含
的信息不會變化,如示例 9-11 所示。
示例 9-11 vector2d_v3_slots.py:只在 Vector2d 類中添加了__slots__ 屬性
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
# 下面是各個方法(因排版需要而省略了)
在類中定義__slots__ 屬性的目的是告訴解釋器:“這個類中的所有實
例屬性都在這兒了!”這樣,Python 會在各個實例中使用類似元組的結
構存儲實例變量,從而避免使用消耗內存的__dict__ 屬性。如果有數
百萬個實例同時活動,這樣做能節省大量內存。
如果要處理數百萬個數值對象,應該使用 NumPy 數組(參見
2.9.3 節)。NumPy 數組能高效使用內存,而且提供了高度優化的數值處理函數,其中很多都一次操作整個數組。我定義 Vector2d
類的目的是討論特殊方法,因為我不太想隨便舉些例子。
在示例 9-12 中,我們運行了兩個構建列表的腳本,這兩個腳本都使用
列表推導創建 10 000 000 個 Vector2d 實例。mem_test.py 腳本的命令行
參數是一個模塊的名字,模塊中定義了不同版本的 Vector2d 類。第一
次運行使用的是 vector2d_v3.Vector2d 類(在示例 9-7 中定義),
第二次運行使用的是定義了__slots__ 的
vector2d_v3_slots.Vector2d 類。
示例 9-12 mem_test.py 使用指定模塊(如 vector2d_v3.py)中定義
的 Vector2d 類創建 10 000 000 個實例
$ time python3 mem_test.py vector2d_v3.py
Selected Vector2d type: vector2d_v3.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 5,623,808
Final RAM usage: 1,558,482,944
real 0m16.721s
user 0m15.568s
sys 0m1.149s
$ time python3 mem_test.py vector2d_v3_slots.py
Selected Vector2d type: vector2d_v3_slots.Vector2d
Creating 10,000,000 Vector2d instances
Initial RAM usage: 5,718,016
Final RAM usage: 655,466,496
real 0m13.605s
user 0m13.163s
sys 0m0.434s
如示例 9-12 所示,在 10 000 000 個 Vector2d 實例中使用__dict__ 屬
性時,RAM 用量高達 1.5GB;而在 Vector2d 類中定義__slots__ 屬
性之后,RAM 用量降到了 655MB。此外,定義了__slots__ 屬性的版
本運行速度也更快。這個測試中使用的 mem_test.py 腳本其實只用于加
載一個模塊、檢查內存用量和格式化結果,所用的代碼與本章沒有太大
關聯,因此放入附錄 A 中的示例 A-4 里。
在類中定義__slots__ 屬性之后,實例不能再有__slots__ 中所列名稱之外的其他屬性。這只是一個副作用,不是__slots__ 存在的真正原因。不要使用__slots__ 屬性禁止類的
用戶新增實例屬性__slots__ 是用于優化的,不是為了約束程序
員。
然而,“節省的內存也可能被再次吃掉”:如果把__dict__這個名稱
添加到__slots__ 中,實例會在元組中保存各個實例的屬性,此外還
支持動態創建屬性,這些屬性存儲在常規__dict__ 中。當然,把__dict__添加到__slots__ 中可能完全違背了初衷,這取決于各個
實例的靜態屬性和動態屬性的數量及其用法。粗心的優化甚至比提早優
化還糟糕。
此外,還有一個實例屬性可能需要注意,即__weakref__ 屬性,為了
讓對象支持弱引用(參見 8.6 節),必須有這個屬性。用戶定義的類中
默認就有__weakref__ 屬性。可是,如果類中定義了__slots__ 屬
性,而且想把實例作為弱引用的目標,那么要把__weakref__添加
到__slots__ 中。
綜上,slots 屬性有些需要注意的地方,而且不能濫用,不能使用
它限制用戶能賦值的屬性。處理列表數據時__slots__ 屬性最有用,
例如模式固定的數據庫記錄,以及特大型數據集。然而,如果你經常處
理大量數據,一定要了解一下 NumPy(http://www.numpy.org);此外,
數據分析庫 pandas(http://pandas.pydata.org)也值得了解,這個庫可以
處理非數值數據,而且能導入 / 導出很多不同的列表數據格式。slots 的問題
總之,如果使用得當__slots__ 能顯著節省內存,不過有幾點要注
意。
每個子類都要定義__slots__ 屬性,因為解釋器會忽略繼承的__slots__ 屬性。
實例只能擁__slots__ 中列出的屬性,除非把__dict__加
入__slots__ 中(這樣做就失去了節省內存的功效)。
如果不把__weakref__
加入__slots__,實例就不能作為弱引
用的目標。
如果你的程序不用處理數百萬個實例,或許不值得費勁去創建不尋常的
類,那就禁止它創建動態屬性或者不支持弱引用。與其他優化措施一
樣,僅當權衡當下的需求并仔細搜集資料后證明確實有必要時,才應該
使用__slots__ 屬性。
本章最后一個話題討論如何在實例和子類中覆蓋類屬性。