Descriptor 是 Python 中最強大但也最容易被忽略的機制之一。理解 Descriptor 是深入 Python 物件模型的關鍵。

先備知識

本章目標

學完本章後,你將能夠:

  1. 理解 @property 實際上是一個 Descriptor
  2. 區分 Data Descriptor 和 Non-data Descriptor
  3. 理解屬性查找順序
  4. 實作自訂的 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)

思考題

  1. 為什麼 __set_name__ 是 Python 3.6 才加入的?之前怎麼解決這個問題?
  2. @property 是 Data Descriptor 還是 Non-data Descriptor?為什麼?
  3. 如果 Descriptor 存在於實例的 __dict__ 中會怎樣?

實作練習

  1. 實作一個 @cached_property 裝飾器
  2. 實作一個範圍驗證的 Descriptor(如 age = Range(0, 150)
  3. 實作一個只讀屬性的 Descriptor

延伸閱讀


下一章:Metaclass 設計與應用