Python __slots__ 介紹與教學
Posted on Oct 16, 2023 in Python 程式設計 - 中階 by Amo Chen ‐ 4 min read
__slots__ 是 Python 類別必須認識的屬性,這個屬性為我們帶來節省記憶體資源以及增加存取(access)類別屬性(attribute)效率的好處,但相對地,它也犧牲原本方便擴充類別屬性的易用性。
總的來說,它是寫出高效率 Python 程式碼的一環,平常用不到它沒有關係,但如果要榨出更多記憶體資源以及效能的話, __slots__ 是一定能派上用場。
本文將透過各種範例認識 __slots__ 並學會如何運用它。
本文環境
- Python 3
- pympler
$ pip install pympler
__slots__ 簡介
This class variable can be assigned a string, iterable, or sequence of strings with variable names used by instances.
__slots__ 是 1 個類別變數,它可以接受 string, iterable 或是多個 string, 而這些值會被作為類別實例化之後可存取的屬性名。
舉例子看最快:
class Point(object):
__slots__ = "x", "y"
當我們實例化 Point 類別之後,就只會有 x 與 y 屬性可以存取,如果要存取 x , y 以外的屬性就會有 AttributeError 產生:
p = Point()
p.x = 0
p.y = 0
p.z = 0 # <--- AttributeError: 'Point' object has no attribute 'z'
除了 __slots__ = "x", "y" 這種指定方式,以下也是 __slots__ 可以接受的方式:
__slots__ = ("x", "y", )
__slots__ = ["x", "y", ]
__slots__ = {"x", "y", }
為什麼要使用 __slots__ ?
Python 類別的實作是預設使用 dictionary 存成員(屬性、方法等),因此帶來記憶體的用量較多以及屬性的存取較慢的問題(因為屬性的存取必須先算 hash 再查找)。
然而,使用 __slots__ 的類別則是使用陣列的方式存屬性,因此在記憶體的用量上以及存取效率上都有所改善:
__slots__allow us to explicitly declare data members (like properties) and deny the creation of__dict__and__weakref__. The space saved over using__dict__can be significant. Attribute lookup speed can be significantly improved as well.
以下用 Pympler 測量一般類別以及使用 __slots__ 類別所佔的記憶體大小,下列定義 1 個屬性只有 x , y 的 Point 類別,並用 asizeof.asizeof() 測量 Point 實例所佔的記憶體大小:
from pympler import asizeof
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(0, 0)
asizeof.asizeof(p)
上述執行結果如下,可以看到 Point 實例約佔 288 bytes:
288 #bytes
同時我們也可以用 dir() 函式查看 Point 實例的成員:
>>> print(dir(Point(0, 0)))
從結果可以看到成員中確實有包含 x, y, __dict__ 以及 __weakref__ :
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'x',
'y']
如果我們進一步在 Point 類別中宣告 __slots__ 並指定只有 x 與 y 2 個屬性,並同樣查看 Point 實例所佔的記憶體大小:
from pympler import asizeof
class Point(object):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(0, 0)
asizeof.asizeof(p)
上述執行結果如下,可以看到 Point 實例約佔 72 bytes, 相較於不使用 __slots__ 的類別而言,省下不少記憶體空間:
72 #bytes
接著用 dir() 函式查看使用 __slots__ 的 Point 實例的成員,查看是否真如官方文件所說 __dict__ 以及 __weakref__ 會消失:
>>> print(dir(Point(0, 0)))
從結果可以看到成員中確實已經沒有 __dict__ 以及 __weakref__ ,取而代之的是 __slots__ :
['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__slots__',
'__str__',
'__subclasshook__',
'x',
'y']
這就是使用 __slots__ 所帶來的變化。
使用 __slots__ 應注意的事項
雖然使用 __slots__ 帶來記憶體用量以及存取屬性的效能優化,但 __slots__ 仍有其不便之處。
無法自由存取額外屬性
例如當我們試圖新增 1 個私有屬性 _dict 在 Point 類別之中,用以計算 Point 與原點 (0, 0) 的距離時,如下列範例:
import math
class Point(object):
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def dist(self):
self._dist = math.sqrt(self.x**2 + self.y**2)
return self._dist
執行時就會出現 AttributeError 找不到屬性 _dist :
8 def dist(self):
----> 9 self._dist = math.sqrt(self.x**2 + self.y**2)
AttributeError: 'Point' object has no attribute '_dist'
這是由於 __slots__ 未定義 _dict 屬性之故,要修好這問題,就必須在 __slots__ 中加入 _dist 字串:
import math
class Point(object):
__slots__ = ('x', 'y', '_dist')
def __init__(self, x, y):
self.x = x
self.y = y
def dist(self):
self._dist = math.sqrt(self.x**2 + self.y**2)
return self._dist
也就是說,使用 __slots__ 會喪失自由存取額外屬性的方便性。
不過 Python 還是有提供使用 __slots__ 能自由存取額外屬性的方法,也就是在 __slots__ 中加入 __dict__ 字串,如下列範例:
class ArbitraryPoint(object):
__slots__ = ('x', 'y', '__dict__')
def __init__(self, x, y):
self.x, self.y = x, y
def dist(self):
self._dist = math.sqrt(self.x**2 + self.y**2)
return self._dist
print(ArbitraryPoint(3, 4).dist())
如此一來, x, y 以外的屬性,都還是用 __dict__ 存,但相對的,我們就喪失想使用 __slots__ 節省記憶體的意義,因此這種使用方式相當少見。
另外,如果有需要使用 __weakref__, 也可以在 __slots__ 中加入 __weakref__ 字串:
class ArbitraryPoint(object):
__slots__ = ('x', 'y', '__dict__', '__weakref__')
__slots__ 會與其他同名類別屬性(class variable)產生衝突
下列範例試圖為 y 屬性建立預設值:
class Point(object):
__slots__ = ('x', 'y')
y = 1
一旦試圖執行上述範例,就會出現 ValueError 並顯示 __slots__ 中的 y 與類別屬性 y 產生衝突:
----> 1 class Point(object):
2 __slots__ = ('x', 'y')
3 y = 1
4
5
ValueError: 'y' in __slots__ conflicts with class variable
這是因為 __slots__ 中定義的屬性也會以類別屬性的方式存在,下列範例就能夠看到即使我們不實例化 Point 類別,也會有 x 與 y 屬性存在:
class Point(object):
__slots__ = ('x', 'y')
print(dir(Point))
上述範例執行結果如下:
['__class__',
'...skipped...',
'x',
'y']
因此使用 __slots__ 要注意同名類別屬性的存在,否則會導致 ValueError , 如果要為 y 設定預設值,可以在 __init__() 方法中設定:
class Point(object):
__slots__ = ('x', 'y')
def __init__(self, x, y=1):
self.x = x
self.y = y
繼承問題
由於 __slots__ 中定義的屬性是以類別屬性的方式存在,因此假設有類別想繼承 Point 類別,並想覆寫 __slots__ 設定,是無法產生作用的,屬性會一併從父類別繼承下來,例如下列類別 PointZ 看起來想要覆寫 Point 類別的 __slots__ 變成只有 z 屬性:
class Point(object):
__slots__ = ('x', 'y')
class PointZ(Point):
__slots__ = ('z', )
但實際上,並無法覆寫。
可以用 dir(PointZ) 檢查看看:
>>> print(dir(PointZ))
['__class__',
'...skipped...',
'x',
'y',
'z']
所以一但我們更改父類別的 __slots__ , 所有的子類別都會受到影響!
這就是使用 __slots__ 要注意的繼承問題。
總結
__slots__ 是 1 個相當有用的工具,特別是在需要優化大量相同類別的記憶體用量的情況下。
就算平常用不到,可以先記起來,等到你遇到瓶頸時,它自然就會在你腦海中浮現!
以上!
Enjoy!