2.1 Descriptor Protocol 完整指南
2.1 Descriptor Protocol 完整指南
Descriptor 是 Python 中最強大但也最容易被忽略的機制之一。理解 Descriptor 是深入 Python 物件模型的關鍵。
先備知識
- 入門系列 4.4 單例與快取(@property 的使用)
本章目標
學完本章後,你將能夠:
- 理解 @property 實際上是一個 Descriptor
- 區分 Data Descriptor 和 Non-data Descriptor
- 理解屬性查找順序
- 實作自訂的 Descriptor
【原理層】@property 的真相
property 是一個 Descriptor
當你使用 @property 時,實際上是建立了一個 Descriptor:
1class Circle:
2 def __init__(self, radius):
3 self._radius = radius
4
5 @property
6 def radius(self):
7 return self._radius
8
9 @radius.setter
10 def radius(self, value):
11 if value < 0:
12 raise ValueError("半徑不能為負")
13 self._radius = value
14
15# 等價於
16class Circle:
17 def __init__(self, radius):
18 self._radius = radius
19
20 def get_radius(self):
21 return self._radius
22
23 def set_radius(self, value):
24 if value < 0:
25 raise ValueError("半徑不能為負")
26 self._radius = value
27
28 radius = property(get_radius, set_radius)property 是 Python 內建的 Descriptor 類別。
Descriptor Protocol
Descriptor 是實現了以下方法之一的物件:
1class Descriptor:
2 def __get__(self, obj, objtype=None):
3 """讀取屬性時呼叫"""
4 pass
5
6 def __set__(self, obj, value):
7 """設定屬性時呼叫"""
8 pass
9
10 def __delete__(self, obj):
11 """刪除屬性時呼叫"""
12 pass
13
14 def __set_name__(self, owner, name):
15 """Python 3.6+:設定屬性名時呼叫"""
16 pass簡單範例
1class Verbose:
2 """一個會報告存取情況的 Descriptor"""
3
4 def __set_name__(self, owner, name):
5 self.name = name
6
7 def __get__(self, obj, objtype=None):
8 if obj is None:
9 return self
10 print(f"讀取 {self.name}")
11 return obj.__dict__.get(self.name)
12
13 def __set__(self, obj, value):
14 print(f"設定 {self.name} = {value}")
15 obj.__dict__[self.name] = value
16
17class MyClass:
18 x = Verbose()
19
20m = MyClass()
21m.x = 10 # 輸出:設定 x = 10
22print(m.x) # 輸出:讀取 x,然後 10【設計層】Data vs Non-data Descriptor
兩種 Descriptor
1# Data Descriptor:有 __set__ 或 __delete__
2class DataDescriptor:
3 def __get__(self, obj, objtype=None):
4 return "data descriptor"
5
6 def __set__(self, obj, value):
7 pass
8
9# Non-data Descriptor:只有 __get__
10class NonDataDescriptor:
11 def __get__(self, obj, objtype=None):
12 return "non-data descriptor"屬性查找順序
這是理解 Descriptor 的關鍵:
1obj.attr 的查找順序:
2
31. Data Descriptor(在類別或父類別中)
42. Instance __dict__
53. Non-data Descriptor(在類別或父類別中)
64. Class __dict__
75. __getattr__(如果定義了) 1class DataDesc:
2 def __get__(self, obj, objtype=None):
3 return "data descriptor"
4 def __set__(self, obj, value):
5 pass
6
7class NonDataDesc:
8 def __get__(self, obj, objtype=None):
9 return "non-data descriptor"
10
11class MyClass:
12 data = DataDesc()
13 nondata = NonDataDesc()
14
15m = MyClass()
16m.__dict__['data'] = "instance value"
17m.__dict__['nondata'] = "instance value"
18
19print(m.data) # data descriptor(Data Descriptor 優先)
20print(m.nondata) # instance value(Instance 優先於 Non-data)為什麼 method 是 Non-data Descriptor?
1class MyClass:
2 def method(self):
3 return "method"
4
5# 可以在實例上覆蓋方法
6m = MyClass()
7m.method = lambda: "overridden"
8print(m.method()) # overridden如果 method 是 Data Descriptor,就無法這樣覆蓋了。
【實作層】實用的 Descriptor
延遲計算屬性
1class LazyProperty:
2 def __init__(self, func):
3 self.func = func
4
5 def __set_name__(self, owner, name):
6 self.name = name
7
8 def __get__(self, obj, objtype=None):
9 if obj is None:
10 return self
11 value = self.func(obj)
12 obj.__dict__[self.name] = value # 快取到實例
13 return value
14
15class Data:
16 def __init__(self, values):
17 self.values = values
18
19 @LazyProperty
20 def average(self):
21 print("計算平均值...")
22 return sum(self.values) / len(self.values)
23
24d = Data([1, 2, 3, 4, 5])
25print(d.average) # 計算平均值... 3.0
26print(d.average) # 3.0(從快取讀取)型別驗證器
1class Typed:
2 def __init__(self, expected_type):
3 self.expected_type = expected_type
4
5 def __set_name__(self, owner, name):
6 self.name = name
7
8 def __get__(self, obj, objtype=None):
9 if obj is None:
10 return self
11 return obj.__dict__.get(self.name)
12
13 def __set__(self, obj, value):
14 if not isinstance(value, self.expected_type):
15 raise TypeError(
16 f"{self.name} 必須是 {self.expected_type.__name__}"
17 )
18 obj.__dict__[self.name] = value
19
20class Person:
21 name = Typed(str)
22 age = Typed(int)
23
24 def __init__(self, name, age):
25 self.name = name
26 self.age = age
27
28p = Person("Alice", 30) # OK
29p = Person("Bob", "thirty") # TypeError!類似 Django Model Field
1class Field:
2 def __init__(self, default=None):
3 self.default = default
4
5 def __set_name__(self, owner, name):
6 self.name = name
7 self.private_name = f"_{name}"
8
9 def __get__(self, obj, objtype=None):
10 if obj is None:
11 return self
12 return getattr(obj, self.private_name, self.default)
13
14 def __set__(self, obj, value):
15 setattr(obj, self.private_name, value)
16
17class CharField(Field):
18 def __init__(self, max_length, **kwargs):
19 super().__init__(**kwargs)
20 self.max_length = max_length
21
22 def __set__(self, obj, value):
23 if len(value) > self.max_length:
24 raise ValueError(f"超過最大長度 {self.max_length}")
25 super().__set__(obj, value)
26
27class User:
28 name = CharField(max_length=50)
29 email = CharField(max_length=100)思考題
- 為什麼
__set_name__是 Python 3.6 才加入的?之前怎麼解決這個問題? @property是 Data Descriptor 還是 Non-data Descriptor?為什麼?- 如果 Descriptor 存在於實例的
__dict__中會怎樣?
實作練習
- 實作一個
@cached_property裝飾器 - 實作一個範圍驗證的 Descriptor(如
age = Range(0, 150)) - 實作一個只讀屬性的 Descriptor
延伸閱讀
下一章:Metaclass 設計與應用