返回文章列表

Python 描述符 Descriptor 高階應用

本文探討 Python 描述符(Descriptor)的進階用法,包含其核心方法 `__get__`、`__set__` 和 `__delete__` 的實作細節,以及如何應用描述符控制屬性存取、驗證輸入值、追蹤屬性變化歷史,並以實際案例展示描述符的泛型設計與程式碼重用技巧。

Python 程式設計

Python 描述符是一種強大的元程式設計工具,允許開發者在不修改類別原始碼的情況下,精細控制屬性的行為。透過實作 __get____set____delete__ 方法,描述符可以攔截屬性存取、修改和刪除操作,實作資料驗證、型別轉換、延遲計算等功能。理解描述符的運作機制,能幫助開發者編寫更簡潔、更具彈性的程式碼。描述符的應用場景廣泛,從簡單的屬性驗證到複雜的 ORM 框架,都能看到它的身影。

深入理解描述符的機制對於 Python 開發者至關重要,它不僅可以提升程式碼的簡潔性和可維護性,更能展現開發者對於 Python 語言的掌握程度。透過描述符,我們可以將通用的邏輯抽象出來,避免程式碼重複,並提高程式碼的可讀性。此外,描述符也提供了一種更優雅的方式來處理屬性的動態計算、驗證和控制,使得程式碼更加健壯和靈活。

描述符(Descriptor)的高階應用與實踐

在 Python 中,描述符(Descriptor)是一種強大且靈活的工具,能夠實作屬性的抽象與重用。本篇文章將探討描述符的核心方法及其在實際開發中的應用。

描述符的基本結構

描述符是一種實作了特定協定的物件,主要包含三個特殊方法:__get____set____delete__。這些方法允許描述符控制屬性的存取、修改和刪除行為。

1. __get__ 方法

__get__ 方法負責取得屬性的值,其簽名如下:

def __get__(self, instance, owner):
    ...

此方法在存取屬性時被呼叫,傳回屬性的值。

2. __set__ 方法

__set__ 方法負責設定屬性的值,其簽名如下:

def __set__(self, instance, value):
    ...

此方法在指定給屬性時被呼叫,用於驗證或轉換輸入的值。

class Validation:
    def __init__(self, validation_function, error_msg):
        self.validation_function = validation_function
        self.error_msg = error_msg

    def __call__(self, value):
        if not self.validation_function(value):
            raise ValueError(f"{value!r} {self.error_msg}")

class DescriptorClass:
    def __init__(self, validations):
        self.validations = validations
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        for validation in self.validations:
            validation(value)
        instance.__dict__[self.name] = value

class ClientClass:
    descriptor = DescriptorClass(
        validations=[
            Validation(lambda x: isinstance(x, (int, float)), "is not a number"),
            Validation(lambda x: x >= 0, "is not >= 0"),
        ]
    )

#### 內容解密:
1. **`Validation` 類別的作用**定義一個可呼叫的驗證器用於檢查輸入值是否符合特定的條件如果驗證失敗則丟擲 `ValueError`。
2. **`DescriptorClass` 的實作**利用 `validations` 列表來儲存多個驗證條件`__set__` 方法中逐一執行這些驗證器確保輸入值的合法性
3. **`ClientClass` 中的應用**透過 `DescriptorClass` 建立一個名為 `descriptor` 的屬性該屬性會自動進行數值型別和範圍的驗證

### 刪除屬性的控制:`__delete__` 方法

`__delete__` 方法允許控制屬性的刪除行為其簽名如下
```python
def __delete__(self, instance):
    ...

此方法在執行 del 操作時被呼叫,可以用來實作許可權控制或其他自定義邏輯。

class ProtectedAttribute:
    def __init__(self, requires_role=None):
        self.permission_required = requires_role
        self._name = None

    def __set_name__(self, owner, name):
        self._name = name

    def __set__(self, user, value):
        if value is None:
            raise ValueError(f"{self._name} can't be set to None")
        user.__dict__[self._name] = value

    def __delete__(self, user):
        if self.permission_required in user.permissions:
            user.__dict__[self._name] = None
        else:
            raise ValueError(f"User {user!s} doesn't have {self.permission_required} permission")

class User:
    email = ProtectedAttribute(requires_role="admin")

    def __init__(self, username: str, email: str, permission_list=None):
        self.username = username
        self.email = email
        self.permissions = permission_list or []

#### 內容解密:
1. **`ProtectedAttribute` 的設計**透過檢查使用者的許可權來決定是否允許刪除屬性如果使用者具有指定的角色"admin"),則將屬性設為 `None`,否則丟擲 `ValueError`。
2. **`User` 類別中的應用**`email` 屬性與 `ProtectedAttribute` 繫結確保只有具備 "admin" 許可權的使用者才能刪除其電子郵件地址

### 自動取得屬性名稱:`__set_name__` 方法

在 Python 3.6 之後描述符新增了 `__set_name__` 方法用於自動取得屬性名稱避免了手動傳遞名稱的麻煩

```python
class DescriptorWithName:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

class ClientClass:
    descriptor = DescriptorWithName()

#### 內容解密:
1. **`__set_name__` 的作用**當描述符被指定給類別屬性時該方法會被呼叫並將屬性名稱傳遞給描述符
2. **簡化程式碼**透過使用 `__set_name__`,無需在初始化描述符時手動指定屬性名稱提高了程式碼的可維護性

## Python 描述符(Descriptor)深入解析

在 Python 中描述符Descriptor是一種強大的工具能夠實作屬性的動態計算驗證和控制描述符本質上是一個實作了特定協定的物件這個協定包括 `__get__`、`__set__``__delete__` 方法

### 描述符的型別

根據描述符實作的方法不同可以將其分為兩類別資料描述符Data Descriptor和非資料描述符Non-Data Descriptor)。

#### 非資料描述符

非資料描述符僅實作了 `__get__` 方法當存取一個物件的屬性時如果該屬性在物件的 `__dict__` 中不存在Python 會查詢類別中的屬性如果找到的是非資料描述符則會呼叫其 `__get__` 方法

```python
class NonDataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42

class ClientClass:
    descriptor = NonDataDescriptor()
>>> client = ClientClass()
>>> client.descriptor
42
>>> client.descriptor = 43
>>> client.descriptor
43
>>> del client.descriptor
>>> client.descriptor
42

資料描述符

資料描述符實作了 __set____delete__ 方法。當存取一個物件的屬性時,如果該屬性是資料描述符,Python 會優先呼叫其 __get____set____delete__ 方法,而不會查詢物件的 __dict__

class DataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return 42

    def __set__(self, instance, value):
        instance.__dict__["descriptor"] = value

class ClientClass:
    descriptor = DataDescriptor()
>>> client = ClientClass()
>>> client.descriptor
42
>>> client.descriptor = 99
>>> client.descriptor
42
>>> vars(client)
{'descriptor': 99}

實作細節

DataDescriptor__set__ 方法中,直接修改了 instance.__dict__["descriptor"]。這裡有幾個值得注意的地方:

  1. 為什麼直接修改 instance.__dict__["descriptor"]?這是因為在這個例子中,描述符不知道自己被指定給哪個屬性名,所以直接使用了 "descriptor" 這個名字。

程式碼解析:

def __set__(self, instance, value):
    logger.debug("setting %s.descriptor to %s", instance, value)
    instance.__dict__["descriptor"] = value

內容解密:

  1. logger.debug 陳述式:記錄了設定的過程,用於除錯。
  2. instance.__dict__["descriptor"] = value 陳述式:將值存入例項的字典中,鍵名為 "descriptor"

描述符(Descriptor)實戰應用

在探討描述符(Descriptor)的運作機制後,本章節將著重於其實際應用,展示如何利用描述符最佳化程式碼。描述符提供了一種有效的方式來解決特定問題,並提高程式碼的可維護性和可讀性。

描述符的應用場景

首先,我們將從一個簡單的例子開始,展示如何使用描述符來解決程式碼重複的問題。這個例子涉及一個普通的類別,其中某些屬性需要追蹤其歷史值。我們將逐步改進這個例子,利用描述符來抽象出重複的邏輯。

不使用描述符的第一個嘗試

假設我們有一個名為 Traveler 的類別,用於表示某個旅遊應用中的旅行者。這個類別需要記錄旅行者目前所在的城市以及他們曾經造訪過的城市。以下是一個初步的實作:

class Traveler:
    def __init__(self, name, current_city):
        self.name = name
        self._current_city = current_city
        self._cities_visited = [current_city]

    @property
    def current_city(self):
        return self._current_city

    @current_city.setter
    def current_city(self, new_city):
        if new_city != self._current_city:
            self._cities_visited.append(new_city)
        self._current_city = new_city

    @property
    def cities_visited(self):
        return self._cities_visited

內容解密:

  • Traveler 類別初始化時,設定旅行者的名稱和目前所在城市,並記錄初始城市到 _cities_visited 列表中。
  • current_city 屬性使用 @property 修飾器定義了 getter 和 setter 方法。當 current_city 被更新時,setter 方法會檢查新城市是否與目前城市不同,如果不同,則將新城市加入 _cities_visited 列表,並更新 _current_city
  • cities_visited 屬性提供了存取 _cities_visited 列表的方法。

這個實作能夠滿足需求,但是如果需要在其他類別或屬性中重複相同的邏輯,就會導致程式碼重複。

使用描述符最佳化

為了避免程式碼重複,我們可以利用描述符來抽象出追蹤屬性歷史值的邏輯。描述符提供了一種乾淨且可重用的解決方案。

程式碼範例待補充

內容解密:

(待補充內容的詳細解說)

描述符的優勢

使用描述符可以帶來多個好處,包括:

  • 程式碼重用:描述符允許我們將通用邏輯抽象出來,在多個類別或屬性中重用。
  • 降低耦合度:相比於使用某些魔術方法或類別裝飾器,描述符提供了一種更乾淨、更模組化的解決方案。
  • 提高可讀性:透過將特定邏輯封裝在描述符中,主類別的程式碼變得更加簡潔和易於理解。

泛型描述器(Generic Descriptor)的實作與應用

在上一節中,我們探討瞭如何利用描述器(Descriptor)來解決特定的問題。本文將介紹如何建立一個泛型的描述器,使其能夠在任何類別中被重複使用。雖然目前的需求並未明確要求這種泛型行為,但透過這個例子,我們可以更深入地瞭解描述器的運作原理和實際應用。

建立泛型描述器

我們的目標是設計一個泛型的描述器,它能夠追蹤另一個屬性的變化,並將這些變化值儲存在一個列表中。下面是實作的程式碼:

class HistoryTracedAttribute:
    def __init__(self, trace_attribute_name: str) -> None:
        self.trace_attribute_name = trace_attribute_name
        self._name = None

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        self._track_change_in_value_for_instance(instance, value)
        instance.__dict__[self._name] = value

    def _track_change_in_value_for_instance(self, instance, value):
        self._set_default(instance)
        if self._needs_to_track_change(instance, value):
            instance.__dict__[self.trace_attribute_name].append(value)

    def _needs_to_track_change(self, instance, value) -> bool:
        try:
            current_value = instance.__dict__[self._name]
        except KeyError:
            return True
        return value != current_value

    def _set_default(self, instance):
        instance.__dict__.setdefault(self.trace_attribute_name, [])

class Traveler:
    current_city = HistoryTracedAttribute("cities_visited")

    def __init__(self, name: str, current_city: str) -> None:
        self.name = name
        self.current_city = current_city

內容解密:

  1. __init__ 方法初始化HistoryTracedAttribute 類別在初始化時接收一個引數 trace_attribute_name,用於指定用來儲存追蹤值的屬性名稱。
  2. __set_name__ 方法:當描述器被指定給一個類別屬性時,Python 會自動呼叫此方法,將屬性名稱存入 self._name
  3. __get____set__ 方法:這兩個方法是描述器的核心。__get__ 用於取得屬性的值,而 __set__ 則用於設定屬性的值,並在設定值的過程中追蹤值的變化。
  4. _track_change_in_value_for_instance 方法:此方法負責追蹤值的變化。如果新的值與當前值不同,則將新值加入到追蹤列表中。
  5. _needs_to_track_change 方法:檢查是否需要追蹤值的變化。如果屬性尚未被初始化或新值與當前值不同,則傳回 True
  6. _set_default 方法:確保追蹤列表的屬性存在,如果不存在則初始化為空列表。

描述器的優點與應用場景

從上述程式碼可以看出,描述器的實作雖然相對複雜,但它使得客戶端類別(如 Traveler)的程式碼變得非常簡潔。更重要的是,這個描述器是完全獨立於業務邏輯的,可以在任何類別中重複使用。

描述器特別適合用於開發函式庫、框架和內部 API,但在業務邏輯中使用則相對較少。