python 描述器(Descriptor)的值存放的三種方法

更新日期: 2024 年 11 月 21 日

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

在 Python 中,描述器是一種強大的機制,用於自定義屬性的行為,例如在屬性被讀取、寫入或刪除時執行額外的邏輯。

然而,描述器的核心功能依賴於如何存放和管理屬性值。

當我們為描述器設計存放屬性值的方式時,需要考慮兩個主要問題:

  1. 屬性值的獨立性:每個物件是否需要獨立的屬性值,還是可以共用一個值。
  2. 存取效能與管理便利性:如何在保持程式易讀性與管理性的同時,實現高效的屬性值存取。

以下,我們將介紹描述器存放屬性值的三種常見方法,並逐一分析它們的特點、適用場景、潛在問題,以及如何改進以更適應實際需求。


第一種:將值存放在 self(描述器實體的屬性)

原理

這種方法將值存放在描述器本身的屬性中(例如 self.props)。

所有使用該描述器的物件會共用同一個存放區,因此設定一個物件的值會影響到其他物件。

實現範例

class SharedStorageDescriptor:
    def __init__(self):
        self.value = None  # 描述器自己的屬性

    def __get__(self, obj, obj_type=None):
        return self.value  # 返回共用值

    def __set__(self, obj, value):
        self.value = value  # 更新共用值


class Cat:
    age = SharedStorageDescriptor()  # 掛載描述器

使用範例

kitty = Cat()
nancy = Cat()

# 設定 kitty 的 age
kitty.age = 18
print(kitty.age)  # 輸出:18

# 讀取 nancy 的 age
print(nancy.age)  # 輸出:18,與 kitty 共用

問題

  • 這種方法會導致所有物件共用同一個屬性值,無法實現屬性值的獨立性。
  • 如果屬性需要針對每個物件單獨管理,這種方法並不合適。

第二種:將值存放在物件本身(object 的屬性)

原理

在描述器中,將值存放於目標物件的 .__dict__ 中,使用固定的 _屬性名稱 來避免直接與描述器衝突。例如,_age 作為儲存屬性。

實現範例

class ObjectStorageDescriptor:
    def __get__(self, obj, obj_type=None):
        return getattr(obj, "_age", None)  # 從物件屬性取值

    def __set__(self, obj, value):
        setattr(obj, "_age", value)  # 將值存入物件屬性


class Cat:
    age1 = ObjectStorageDescriptor()  # 第一個描述器
    age2 = ObjectStorageDescriptor()  # 第二個描述器

使用範例

kitty = Cat()

# 設定 age1 屬性
kitty.age1 = 18
print(kitty.age1)  # 輸出:18

# 設定 age2 屬性
kitty.age2 = 20
print(kitty.age2)  # 輸出:20

# 檢查屬性儲存狀態
print(kitty.__dict__)  # 輸出:{'_age': 20}

問題

如你提到的,這樣的實現有一個問題:所有描述器都使用相同的 _age 屬性來存放值,導致屬性之間互相覆蓋。例如:

kitty.age1 = 18
kitty.age2 = 20

print(kitty.age1)  # 輸出:20,因為 `_age` 被覆蓋

第三種:將值存放在描述器的字典,搭配屬性名稱

原理

為了解決前述方法的問題,我們可以使用描述器的內部字典來管理屬性值,並將物件和屬性名稱作為鍵來存取屬性值。這樣既能集中管理值,也能保證每個屬性是獨立的。

實現範例

class DescriptorWithDictionary:
    def __init__(self, name):
        self.storage = {}  # 使用普通字典存放值
        self.name = name   # 描述器名稱,作為鍵的一部分

    def __get__(self, obj, obj_type=None):
        if obj is None:
            return None
        return self.storage.get((obj, self.name), None)  # 從字典取值

    def __set__(self, obj, value):
        self.storage[(obj, self.name)] = value  # 存入字典


class Cat:
    age1 = DescriptorWithDictionary("age1")
    age2 = DescriptorWithDictionary("age2")

使用範例

kitty = Cat()
nancy = Cat()

# 設定 kitty 的屬性
kitty.age1 = 18
kitty.age2 = 20

# 設定 nancy 的屬性
nancy.age1 = 25

# 獨立管理
print(kitty.age1)  # 輸出:18
print(kitty.age2)  # 輸出:20
print(nancy.age1)  # 輸出:25

# 檢查字典內容(進階觀察用)
print(Cat.age1.storage)  
# 輸出:{(<Cat object>, 'age1'): 18, (<Cat object>, 'age1'): 25}

優點

  1. 屬性值集中管理於描述器內部。
  2. 每個物件的屬性值是獨立的,不會互相覆蓋。
  3. 使用普通字典,簡化了實現。

缺點

  • 使用 (obj, 屬性名稱) 作為鍵,可能對大型應用略有性能影響。

總結

儲存方式優點缺點
存在 self集中管理,適合全局屬性所有物件共用值,無法實現屬性獨立
存在物件的 .__dict__每個物件屬性獨立,直觀易懂屬性名稱可能衝突,需額外管理唯一名稱
存在描述器的字典集中管理,屬性完全獨立,實現簡單使用字典稍有性能開銷,但通常可以接受

Similar Posts