python 描述器(Descriptor)的值存放的三種方法
更新日期: 2024 年 11 月 21 日
本文為描述器系列文,第三篇
- 基礎:深入理解 Python (Descriptor)描述器的運作與應用
- 1 階進化:Python 描述器進階:資料描述器與非資料描述器的區別與屬性查找順序
- 2 階進化: python 描述器(Descriptor)的值存放的三種方法
在 Python 中,描述器是一種強大的機制,用於自定義屬性的行為,例如在屬性被讀取、寫入或刪除時執行額外的邏輯。
然而,描述器的核心功能依賴於如何存放和管理屬性值。
當我們為描述器設計存放屬性值的方式時,需要考慮兩個主要問題:
- 屬性值的獨立性:每個物件是否需要獨立的屬性值,還是可以共用一個值。
- 存取效能與管理便利性:如何在保持程式易讀性與管理性的同時,實現高效的屬性值存取。
以下,我們將介紹描述器存放屬性值的三種常見方法,並逐一分析它們的特點、適用場景、潛在問題,以及如何改進以更適應實際需求。
第一種:將值存放在 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}
優點
- 屬性值集中管理於描述器內部。
- 每個物件的屬性值是獨立的,不會互相覆蓋。
- 使用普通字典,簡化了實現。
缺點
- 使用
(obj, 屬性名稱)
作為鍵,可能對大型應用略有性能影響。
總結
儲存方式 | 優點 | 缺點 |
---|---|---|
存在 self | 集中管理,適合全局屬性 | 所有物件共用值,無法實現屬性獨立 |
存在物件的 .__dict__ | 每個物件屬性獨立,直觀易懂 | 屬性名稱可能衝突,需額外管理唯一名稱 |
存在描述器的字典 | 集中管理,屬性完全獨立,實現簡單 | 使用字典稍有性能開銷,但通常可以接受 |