Python 描述器進階:資料描述器與非資料描述器的區別與屬性查找順序

更新日期: 2024 年 11 月 21 日

本文為描述器系列文,第三篇

描述器是 Python 中控制屬性存取行為的強大工具,分為資料描述器(Data Descriptor)非資料描述器(Non-Data Descriptor)兩類。

它們在處理屬性時的行為和優先順序有所不同,進而影響屬性查找的結果。

本文將詳細介紹這兩類描述器的特性,並通過範例說明屬性查找順序如何影響描述器的行為。


背景知識:資料描述器與非資料描述器

非資料描述器(Non-Data Descriptor)

  • 定義:只實作了 __get__() 方法,沒有實作 __set__() 方法。
  • 行為
    • 可以處理屬性的「讀取」行為。
    • 無法處理屬性的「寫入」行為。
    • 當寫入屬性時,Python 會直接將值存入物件的 .__dict__ 中,這會覆蓋非資料描述器的行為。

資料描述器(Data Descriptor)

  • 定義:同時實作了 __get__()__set__() 方法。
  • 行為
    • 可以同時處理屬性的「讀取」與「寫入」行為。
    • 資料描述器的優先順序高於物件的 .__dict__,確保屬性值由描述器控制。

屬性查找順序

Python 在存取屬性時,會依以下順序進行查找:

  1. 物件的 .__dict__(物件自身的屬性存儲區域)。
  2. 類別的資料描述器(實作了 __get__()__set__() 的描述器)。
  3. 類別的非資料描述器(只實作了 __get__() 的描述器)。
  4. 類別的其他屬性或方法

關鍵點

  • 如果 .__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__ 被調用
描述器的值
物件自己的值

說明

  1. 第一次存取 obj.attr
    • Python 在類別上找到 attr,這是一個非資料描述器。
    • 調用 NonDataDescriptor.__get__(),返回 "描述器的值"
  1. 寫入 obj.attr = "物件自己的值"
    • 由於非資料描述器沒有實作 __set__() 方法,Python 將屬性值直接存入物件的 .__dict__
  1. 再次存取 obj.attr
    • Python 首先查找 obj.__dict__,發現該屬性已存在,值為 "物件自己的值"
    • Python 不再調用 NonDataDescriptor.__get__(),直接返回 .__dict__ 的值。

資料描述器如何避免覆蓋

資料描述器(實作了 __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

說明

  1. 設置 obj.attr = 10
    • 資料描述器的 __set__() 方法被觸發。
    • 驗證數值是否合法後,將值儲存到物件的 _value 屬性。
  1. 存取 obj.attr
    • 資料描述器的 __get__() 方法被觸發,返回 _value 的值。
  1. 為什麼資料描述器不會被覆蓋
    • 資料描述器的優先順序高於物件的 .__dict__,屬性值始終由描述器控制。

親近生活的比喻

非資料描述器

想像一位「顧問」,專門提供意見(屬性讀取)。如果你在辦公桌上放了一份文件(屬性寫入),之後你只會看到桌上的文件,而不再詢問顧問。

資料描述器

資料描述器像是「主管」,不僅負責回答問題(屬性讀取),也負責檢查和修改文件(屬性寫入)。主管的意見優先於桌上的文件,確保辦公規範不被破壞。


總結

非資料描述器

  • 特點:只實作 __get__(),控制屬性的讀取行為。
  • 限制:當寫入屬性時,Python 直接更新 .__dict__,覆蓋描述器行為。

資料描述器

  • 特點:實作 __get__()__set__(),同時控制屬性的讀取與寫入行為。
  • 優勢:資料描述器的優先順序高於 .__dict__,避免屬性行為被覆蓋。

屬性查找順序

  1. .__dict__:物件自身的屬性存儲區域。
  2. 資料描述器:具有最高優先順序。
  3. 非資料描述器:優先順序低於 .__dict__
  4. 類別的其他屬性

理解描述器的運作機制與查找順序,能幫助我們在需要更精細屬性管理的場景中,做出更加靈活且穩健的設計!

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *