【Python】詳解 __slots__

目錄

一、說明

1.1 限制用戶動態修改類成員

1.2 減少內存額外消耗 / 提升屬性訪問速度

1.3 小結

二、從另一條路線看待 __slots__ 的影響 (選讀)


一、說明

1.1 限制用戶動態修改類成員

Python 作爲一門十分靈活的 動態語言,多處設計爲顧及靈活而犧牲效率/性能。例如,Python 作爲動態語言,類創建好後仍可動態創建類成員 (字段/方法/屬性)。這在靜態語言中無法實現,只能調用類中已有屬性,而難以甚至無法添加新屬性。

>>> class Point:
	def __init__(self, x=0, y=0):
		self.x = x
		self.y = y

>>> p = Point()
>>> p.z
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>
    p.z
AttributeError: 'Point' object has no attribute 'z'
>>> p.z = 1
>>> p.z
1

上例構造了一個 Point 類,具有屬性 x 和 y。實例化 Point 類對象後,實例 p 雖然本無屬性 z,但通過 p.z = 1 的賦值操作卻可直接將屬性 z 添加至實例 p 中。這雖然很靈活,但從反面看卻存在隱患。若用戶具有隨意添加屬性的權限,就可能導致未知問題,特別是面對複雜系統。因此,有時出於嚴謹,並不希望用戶能夠隨意動態修改。這時,__slots__ 應運而生。

>>> class Point_new:
	__slots__ = ['x', 'y']        # 限制使用
	def __init__(self, x=0, y=0):
		self.x = x
		self.y = y

>>> q = Point_new()
>>> q.z
Traceback (most recent call last):
  File "<pyshell#27>", line 1, in <module>
    q.z
AttributeError: 'Point_new' object has no attribute 'z'
>>> q.z = 1
Traceback (most recent call last):
  File "<pyshell#28>", line 1, in <module>
    q.z = 1
AttributeError: 'Point_new' object has no attribute 'z'

可見,實例只能使用 關鍵字 __slots__ 中定義/聲明的成員屬性 x 和 y,屬性 z 原本就不存在,用類 Point_new 的實例 q 也無法通過賦值的方式再創建。換言之,對實例屬性而言,類屬性是只讀的,不可以通過實例對屬性進行增刪改 (而只能通過類增刪改類屬性,並隨之影響實例屬性,詳見第二節)。從而,使用 __slots__ 能夠 限制用戶隨意動態修改成員

此外,__slots__ 的另一個功能是 減少內存消耗,提升屬性訪問速度

在 Python 底層實現中,默認使用一個個 命名空間字典(namespace dictionary) __dict__ 來保存類的實例屬性,從而允許在運行時動態創建任意新成員 (字段/方法/屬性)。可令實例直接調用 __dict__ 觀察 (承接上例):

# 未使用 __slots__
>>> p.__dict__
{'x': 0, 'y': 0, 'z': 1}

# 已使用 __slots__
>>> q.__dict__
Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    q.__dict__
AttributeError: 'Point_new' object has no attribute '__dict__'

可見,使用 __slots__ 前,類實例調用 __dict__ 可查看實例屬性,但使用  __slots__ 後就不可以了。

使用 __slots__ 前,Python 無法在創建實例時直接分配⼀個固定量的內存來保存所有的實例屬性。因此,對於許多“小”類對象而言,使用 dict 維護實例將額外存儲許多數據,佔用大量不必要的內存,特別是創建成千上萬的實例時。

使用 __slots__ 後,Python 內部的 __new__ 方法將不再創建一個 dict 來保存實例屬性,而是以一個 固定大小的數組 取而代之,從而節省空間。

事實上,Python 內置的 dict 本質是一個哈希表 (hashtable),是一種 用空間換時間 的數據結構。爲解決衝突問題,當 dict 使用量超過 2/3 時,Python 會根據情況進行 2-4 倍的擴容。由此又佐證了取消 __dict__ 的使用可大幅減少實例的空間消耗。通常,使用 __slots__ 能 降低 40%~50% 的內存佔用率,即 犧牲了一定的靈活性來保證性能。這也是 __slots__ 關鍵字的設計初衷。

>>> class Point_new_son(Point_new):
	pass

>>> g = Point_new_son()
>>> g.z
Traceback (most recent call last):
  File "<pyshell#39>", line 1, in <module>
    g.z
AttributeError: 'Point_new_son' object has no attribute 'z'
>>> g.z = 2

注意,__slots__ 定義的屬性僅對當前類實例起作用,而對繼承的子類無效,如上例所示。除非子類中也定義 __slots__ ,從而令子類實例允許定義的屬性爲自身的 __slots__  加上父類的 __slots__ 。

1.2 減少內存額外消耗 / 提升屬性訪問速度

接下來,使用一個例子來定量分析使用  __slots__ 關鍵字帶來的內存性能提升:

首先安裝 ipython_memory_usage 模塊 用於在 Jupyter Notebook 上精確地展示運行時間與內存佔用情況。命令行輸入:

$ pip install ipython_memory_usage

或用 conda 安裝:

$ conda install -c conda-forge ipython_memory_usage

然後打開 Jupyter Notebook,新建一個 py3 主文件,導入模塊並啓動內存觀測功能:

自此,每個 ceil 執行完都將按上述格式返回運行時間與內存佔用情況,以便於觀察。

接着,另新建一個 without_slots.py 模塊,存放一個未使用 __slots__ 關鍵字的示例:

在主文件中導入 without_slots.py 模塊,並隨之顯示運行情況: 

接着,另新建一個 with_slots.py 模塊,存放一個使用了 __slots__ 關鍵字的示例:

在主文件中導入 with_slots.py 模塊,並隨之顯示運行情況:

對比可見,使用 __slots__ 關鍵字減輕了將近 60% 的內存負擔,內存佔用率幾乎至少有 40%~50% 的降低。

1.3 小結

總而言之,__slots__ 關鍵字的使用能夠帶來如下好處:

  • 屬性聲明 (Attribute Declaration),限制動態修改成員
  • 減少內存額外消耗,提升屬性訪問速度

此外,__slots__ 的相關用法細節和注意事項還有不少,此處暫不贅述。下節將通過示例,從另一條路線看待 __slots__ 帶來的變化,原理基於本節。

二、從另一條路線看待 __slots__ 的影響 (選讀)

首先,沿用上文,創建一個類 Point,再實例化一個 Point 類對象 p0:

>>> class Point:
	def __init__(self, x=0, y=0):
		self.x = x
		self.y = y

>>> Point.__dict__  # 類的 dict 包含了當前類的類屬性
mappingproxy({'__module__': '__main__', '__init__': <function Point.__init__ at 0x00000214BB1226A8>, '__dict__': <attribute '__dict__' of 'Point' objects>, '__weakref__': <attribute '__weakref__' of 'Point' objects>, '__doc__': None})

>>> p0 = Point()  # 實例的 dict 包含了當前實例的實例屬性
>>> p0.__dict__
{'x': 0, 'y': 0}

可見,Point 類的 dict 包含了當前的類屬性,而 Point 類實例 p0 的 dict 包含了當前的實例屬性二者有所不同

對同一個類而言,理論上可以創建任意數量的實例,但每個實例所攜帶的 dict 積少成多,將是一筆不小的開銷。爲此,用於控制 dict 的 slots 應運而生。例如,再創建一個類 Point,並在構造函數 __init__() 前先使用 __slots__ 關鍵字聲明屬性:

>>> class Point:
	__slots__ = ('x', 'y')
	def __init__(self, x=0, y=0):
		self.x = x
		self.y = y

>>> dir(Point)  # 類不再維護類屬性 __dict__ 了 (__dict__ 不見了)
['__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']

>>> p1 = Point()
>>> p1.__dict__  # 實例的 命名空間字典(namespace dictionary) dict 消失了
Traceback (most recent call last):
  File "<pyshell#29>", line 1, in <module>
    p1.__dict__
AttributeError: 'Point' object has no attribute '__dict__'

將類 Point 實例化爲 p1,可見 p1 的 dict 也消失了。再將類 Point 實例化爲 p2,令三者進行對比:

>>> p2 = Point()

>>> Point.__slots__  # 可見類及其實例的 __slots__ 屬性完全相同
('x', 'y')
>>> p1.__slots__
('x', 'y')
>>> p2.__slots__
('x', 'y')

>>> id(p1.__slots__)  # 可見類及其實例的 __slots__ 內存地址完全相同
2288061097288
>>> id(p2.__slots__)
2288061097288
>>> id(Point.__slots__)
2288061097288

 可以看到,可見類 Point 及其實例 p1 和 p2 的 __slots__ 屬性與內存地址完全相同,這意味着對同一類創建新的實例不會再增加新的 dict 用於保存實例屬性,而是使用同一個類的 slots。

>>> p1.x, p1.y
(0, 0)
>>> p2.x, p2.y
(0, 0)

>>> p1.x = 2  # 實例屬性不可修改 —— 只讀
Traceback (most recent call last):
  File "<pyshell#50>", line 1, in <module>
    p1.x = 2
AttributeError: 'Point' object attribute 'x' is read-only

然而,此時無法再通過實例來修改各自的實例屬性了。因爲對實例屬性而言,類的靜態數據是只讀的、無法修改的,只有通過類屬性才能修改 (對於尚未賦值的屬性,還是能夠通過實例賦值和修改,但不會影響類屬性的值)。換言之,類屬性此時對實例屬性具有單向的決定性作用。例如:

>>> Point.x = 2  # 通過類 Point 修改類屬性 x, 將隨之影響其實例屬性
>>> Point.x
2
>>> p1.x  # p1.x 本爲 0
2
>>> p2.x  # p2.x 本爲 0
2

可見,修改了類屬性,實例屬性也隨之變化。但反之不可,因爲對實例來說,通過類定義的屬性都是隻讀的。

若還要增加實例屬性,也只能通過類增加類屬性實現,例如:

>>> Point.z = 3  # 通過類 Point 新增類屬性 z, 隨之影響其實例屬性
>>> p1.z  # 實例 p1 本無屬性 z
3
>>> p2.z  # 實例 p2 本無屬性 z
3

因此,類通過 slots 牢牢控制了其實例的屬性, 體現了 slots 限制用戶修改 + 優化內存的作用


參考文獻:

《Intermediate Python》、《Python Cookbook》

https://www.cnblogs.com/techflow/p/12747480.html

https://www.cnblogs.com/rainfd/p/slots.html

https://www.cnblogs.com/johnyang/p/10463138.html

https://www.liaoxuefeng.com/wiki/1016959663602400/1017501655757856

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章