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!