Python 描述器進階:資料描述器與非資料描述器的區別與屬性查找順序
更新日期: 2024 年 11 月 21 日
本文為描述器系列文,第三篇
- 基礎:深入理解 Python (Descriptor)描述器的運作與應用
- 1 階進化:Python 描述器進階:資料描述器與非資料描述器的區別與屬性查找順序
- 2 階進化: python 描述器(Descriptor)的值存放的三種方法
描述器是 Python 中控制屬性存取行為的強大工具,分為資料描述器(Data Descriptor)和非資料描述器(Non-Data Descriptor)兩類。
它們在處理屬性時的行為和優先順序有所不同,進而影響屬性查找的結果。
本文將詳細介紹這兩類描述器的特性,並通過範例說明屬性查找順序如何影響描述器的行為。
背景知識:資料描述器與非資料描述器
非資料描述器(Non-Data Descriptor)
- 定義:只實作了
__get__()
方法,沒有實作__set__()
方法。 - 行為:
- 可以處理屬性的「讀取」行為。
- 無法處理屬性的「寫入」行為。
- 當寫入屬性時,Python 會直接將值存入物件的
.__dict__
中,這會覆蓋非資料描述器的行為。
資料描述器(Data Descriptor)
- 定義:同時實作了
__get__()
和__set__()
方法。 - 行為:
- 可以同時處理屬性的「讀取」與「寫入」行為。
- 資料描述器的優先順序高於物件的
.__dict__
,確保屬性值由描述器控制。
屬性查找順序
Python 在存取屬性時,會依以下順序進行查找:
- 物件的
.__dict__
(物件自身的屬性存儲區域)。 - 類別的資料描述器(實作了
__get__()
和__set__()
的描述器)。 - 類別的非資料描述器(只實作了
__get__()
的描述器)。 - 類別的其他屬性或方法。
關鍵點
- 如果
.__dict__
中已經有該屬性,Python 不會調用非資料描述器的__get__()
方法。 - 資料描述器的優先順序高於
.__dict__
,因此能避免被覆蓋。
非資料描述器的行為與限制
當描述器只實作了 __get__()
方法時,寫入屬性會直接影響物件的 .__dict__
,導致描述器邏輯被覆蓋。
以下範例展示了非資料描述器的行為。
實際範例
class NonDataDescriptor:
def __get__(self, obj, obj_type=None):
print("非資料描述器的 __get__ 被調用")
return "描述器的值"
class MyClass:
attr = NonDataDescriptor() # 設定一個非資料描述器
# 創建物件
obj = MyClass()
# 初次存取描述器屬性
print(obj.attr) # 輸出:"描述器的值",觸發 __get__
# 嘗試寫入屬性
obj.attr = "物件自己的值" # 寫入到物件的 __dict__
# 再次存取描述器屬性
print(obj.attr) # 輸出:"物件自己的值",因為 __dict__ 優先
執行結果
非資料描述器的 __get__ 被調用
描述器的值
物件自己的值
說明
- 第一次存取
obj.attr
時:- Python 在類別上找到
attr
,這是一個非資料描述器。 - 調用
NonDataDescriptor.__get__()
,返回"描述器的值"
。
- Python 在類別上找到
- 寫入
obj.attr = "物件自己的值"
:- 由於非資料描述器沒有實作
__set__()
方法,Python 將屬性值直接存入物件的.__dict__
。
- 由於非資料描述器沒有實作
- 再次存取
obj.attr
時:- Python 首先查找
obj.__dict__
,發現該屬性已存在,值為"物件自己的值"
。 - Python 不再調用
NonDataDescriptor.__get__()
,直接返回.__dict__
的值。
- Python 首先查找
資料描述器如何避免覆蓋
資料描述器(實作了 __get__()
和 __set__()
方法)能控制屬性的「讀取」與「寫入」行為,並且具有比 .__dict__
更高的優先順序。
實際範例
class DataDescriptor:
def __get__(self, obj, obj_type=None):
print("資料描述器的 __get__ 被調用")
return obj._value
def __set__(self, obj, value):
print("資料描述器的 __set__ 被調用")
if value < 0:
raise ValueError("屬性值不能為負數")
obj._value = value
class MyClass:
attr = DataDescriptor() # 設定一個資料描述器
# 創建物件
obj = MyClass()
# 設置屬性值
obj.attr = 10 # 觸發 __set__
print(obj.attr) # 觸發 __get__
# 嘗試寫入負值
# obj.attr = -5 # 引發 ValueError
執行結果
資料描述器的 __set__ 被調用
資料描述器的 __get__ 被調用
10
說明
- 設置
obj.attr = 10
時:- 資料描述器的
__set__()
方法被觸發。 - 驗證數值是否合法後,將值儲存到物件的
_value
屬性。
- 資料描述器的
- 存取
obj.attr
時:- 資料描述器的
__get__()
方法被觸發,返回_value
的值。
- 資料描述器的
- 為什麼資料描述器不會被覆蓋:
- 資料描述器的優先順序高於物件的
.__dict__
,屬性值始終由描述器控制。
- 資料描述器的優先順序高於物件的
親近生活的比喻
非資料描述器
想像一位「顧問」,專門提供意見(屬性讀取)。如果你在辦公桌上放了一份文件(屬性寫入),之後你只會看到桌上的文件,而不再詢問顧問。
資料描述器
資料描述器像是「主管」,不僅負責回答問題(屬性讀取),也負責檢查和修改文件(屬性寫入)。主管的意見優先於桌上的文件,確保辦公規範不被破壞。
總結
非資料描述器
- 特點:只實作
__get__()
,控制屬性的讀取行為。 - 限制:當寫入屬性時,Python 直接更新
.__dict__
,覆蓋描述器行為。
資料描述器
- 特點:實作
__get__()
和__set__()
,同時控制屬性的讀取與寫入行為。 - 優勢:資料描述器的優先順序高於
.__dict__
,避免屬性行為被覆蓋。
屬性查找順序
.__dict__
:物件自身的屬性存儲區域。- 資料描述器:具有最高優先順序。
- 非資料描述器:優先順序低於
.__dict__
。 - 類別的其他屬性。
理解描述器的運作機制與查找順序,能幫助我們在需要更精細屬性管理的場景中,做出更加靈活且穩健的設計!