Python 的裝飾器提供了一種優雅的方式來擴充套件函式和方法的功能,而無需修改原始程式碼。優秀的裝飾器應具備良好的封裝性,將額外邏輯與核心業務分離,並保持正交性,確保各個功能模組相互獨立。同時,可重用性也是評估裝飾器品質的重要指標,通用的裝飾器可以應用於多個場景,減少程式碼冗餘。Celery 和 Web 框架中的裝飾器就是很好的例子,它們簡化了任務定義和路由設定,提升了開發效率。

在實際應用中,我們應該遵循一些最佳實踐來設計和使用裝飾器。首先,保持簡單性,每個裝飾器只專注於單一功能,避免過於複雜的邏輯。其次,提供乾淨的介面,將內部細節隱藏起來,讓使用者更容易理解和使用。此外,遵循 DRY 原則(Don’t Repeat Yourself),避免程式碼重複,提高程式碼的可維護性。最後,確保程式碼具有良好的可讀性,使用清晰的命名和註解,方便其他人理解程式碼的意圖。

分析優秀的裝飾器

本章的最後,讓我們透過分析優秀的裝飾器,瞭解它們在實際應用中的使用方式。接下來,我們將檢視 Python 本身以及流行的函式庫中如何使用裝飾器,以獲得建立良好裝飾器的指導方針。

在探討範例之前,我們先來辨別優秀裝飾器應具備的特性:

  1. 封裝或關注點分離:一個優秀的裝飾器應該有效地將不同的責任區分開來,無論是它本身的功能還是它所裝飾的物件。它不能是一個漏抽象(leaky abstraction),意味著裝飾器的客戶端應該能夠以黑箱模式呼叫它,而無需瞭解其實作邏輯。
  2. 正交性:裝飾器所執行的操作應該是獨立的,並且盡可能與被裝飾的物件解耦。
  3. 可重用性:裝飾器應該能夠應用於多種型別,而不僅僅出現在一個函式的例項上,因為如果它只出現在一個例項上,那就意味著它本來可以只是一個函式。可重用性意味著它必須足夠通用。

Celery 專案提供了一個優秀的裝飾器範例,其中任務是透過將應用程式的任務裝飾器應用於函式來定義的:

@app.task
def mytask():
    ....

這個裝飾器之所以優秀,是因為它非常擅長封裝。函式庫的使用者只需要定義函式主體,裝飾器就會自動將其轉換為任務。@app.task 裝飾器包裝了大量邏輯和程式碼,但這些對於 mytask() 的主體來說是無關緊要的。它實作了完整的封裝和關注點分離——沒有人需要檢視該裝飾器的內部實作,因此它是一個正確的抽象,不會洩露任何細節。

另一個常見的裝飾器使用場景是在 Web 框架中(例如 Pyramid、Flask 和 Sanic),檢視處理器透過裝飾器註冊到 URL:

@route("/", method=["GET"])
def view_handler(request):
    ...

這些型別的裝飾器具有與之前相同的考慮;它們也提供了完全的封裝,因為 Web 框架的使用者很少(如果有的話)需要知道 @route 裝飾器正在做什麼。在這種情況下,我們知道裝飾器正在做一些額外的事情,例如將這些函式註冊到 URL 的對映器,並且它還改變了原始函式的簽名,為我們提供了一個更友好的介面,接收一個已經設定好所有資訊的請求物件。

前面的兩個範例足以讓我們注意到關於這種裝飾器使用的另一個特點。它們符合某種 API。這些函式庫或框架透過裝飾器向使用者公開其功能,事實證明裝飾器是定義乾淨程式設計介面的極佳方式。

這可能是我們應該思考裝飾器的最佳方式。就像事件屬性如何被處理的類別裝飾器的範例一樣,一個好的裝飾器應該提供一個乾淨的介面,讓程式碼的使用者知道從裝飾器中可以期待什麼,而無需瞭解它是如何工作的,或是它的任何細節。

內容解密:

上述內容闡述了建立優秀裝飾器的原則,包括封裝性、正交性和可重用性,並給出了 Celery 和 Web 框架中的實際範例。此外,還強調了在建立函式裝飾器時應使其簽名與原始函式相匹配,以提高可讀性和可維護性。

裝飾器的最佳實踐

  1. 保持簡單:優秀的裝飾器應該只做一件事情,並且做好。
  2. 提供乾淨的介面:透過封裝複雜性,為使用者提供簡單直觀的介面。
  3. 遵循 DRY 原則:透過重用程式碼,避免重複勞動。
  4. 提高可讀性:透過提供清晰簡潔的介面,幫助讀者理解程式碼的功能。

內容解密:

上述最佳實踐闡述瞭如何建立優秀的裝飾器,包括保持簡單、提供乾淨的介面、遵循 DRY 原則和提高可讀性。這些原則有助於建立可維護、可擴充套件和易於理解的程式碼。

下一步

在下一章中,我們將探討 Python 的另一個進階功能——描述符(Descriptors)。特別是,我們將瞭解如何藉助描述符建立更好的裝飾器,並解決本章中遇到的一些問題。

參考資料

以下是一些您可以參考的資訊:

  • PEP-318:函式和方法的裝飾器(https://www.python.org/dev/peps/pep-0318/)
  • PEP-3129:類別裝飾器(https://www.python.org/dev/peps/pep-3129/)
  • WRAPT 01:https://pypi.org/project/wrapt/
  • WRAPT 02:https://wrapt.readthedocs.io/en/latest/decorators.html#universal-decorators
  • Functools 模組:Python 標準函式庫中 functools 模組的 wraps 函式(https://docs.python.org/3/library/functools.html#functools.wrap)
  • ATTRS 01:attrs 函式庫(https://pypi.org/project/attrs/)
  • PEP-557:資料類別(https://www.python.org/dev/peps/pep-0557/)
  • GLASS 01:Robert L. Glass 撰寫的《軟體工程的事實和謬誤》(Facts and Fallacies of Software Engineering)
  • DESIG01:Erich Gamma 撰寫的《設計模式:可複用物件導向軟體的元素》(Design Patterns: Elements of Reusable Object-Oriented Software)
  • PEP-614:放寬對裝飾器的語法限制(https://www.python.org/dev/peps/pep-0614/)

內容解密:

上述參考資料提供了關於 Python 裝飾器的更多資訊,包括官方 PEP 檔案、相關函式庫和設計模式書籍。這些資源有助於深入瞭解 Python 裝飾器的設計原理和最佳實踐。

深入理解Python描述器:物件的強大抽象工具

Python中的描述器(Descriptor)是一種強大的工具,能夠將物件導向程式設計提升到另一個層次。描述器不是其他語言中常見的概念,因此沒有直接的類別比或平行概念可供參考。在本章中,我們將探討描述器的概念、工作原理以及如何有效地實作它們。

描述器的基本概念

描述器是一種物件,它是實作了描述器協定的類別的例項。描述器協定至少包含以下魔術方法(magic methods)之一:

  • __get__
  • __set__
  • __delete__
  • __set_name__

在描述器的實作中,我們需要至少兩個類別:客戶端類別(ClientClass)和描述器類別(DescriptorClass)。客戶端類別是使用描述器功能的類別,而描述器類別則實作了描述器的邏輯。

描述器命名慣例

在本章中,我們將使用以下命名慣例:

名稱意義
ClientClass使用描述器功能的客戶端類別
DescriptorClass實作描述器的類別
clientClientClass的例項
descriptorDescriptorClass的例項,作為ClientClass的類別屬性

描述器的工作原理

當我們存取一個物件的屬性時,通常會直接獲得該屬性的值。但是,如果該屬性是一個描述器,則會發生不同的情況。當客戶端請求該屬性時,Python會呼叫描述器的__get__方法,而不是直接傳回描述器物件本身。

簡單的描述器範例

以下是一個簡單的描述器範例,它只記錄有關上下文的資訊並傳回客戶端物件本身:

class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        logger.info(
            "Call: %s.__get__(%r, %r)",
            self.__class__.__name__,
            instance,
            owner
        )
        return instance

class ClientClass:
    descriptor = DescriptorClass()

當我們執行這段程式碼並請求ClientClass例項的descriptor屬性時,我們會發現實際上獲得的是__get__方法的傳回值,而不是DescriptorClass的例項。

#### 內容解密:

在這個範例中,DescriptorClass實作了__get__方法,該方法記錄了有關上下文的資訊並傳回客戶端物件本身。ClientClass則包含了一個descriptor屬性,它是DescriptorClass的例項。當我們存取ClientClass例項的descriptor屬性時,Python會呼叫DescriptorClass__get__方法,並傳回其傳回值。

描述器的型別

描述器有兩種型別:資料描述器(Data Descriptor)和非資料描述器(Non-Data Descriptor)。資料描述器實作了__set____delete__方法,而非資料描述器只實作了__get__方法。

資料描述器與非資料描述器的差異

資料描述器和非資料描述器的主要差異在於它們對屬性的控制程度。資料描述器可以控制屬性的設定和刪除,而非資料描述器只能控制屬性的取得。

描述器的應用

描述器在Python中有許多應用,例如:

  • 實作屬性驗證和轉換
  • 實作延遲載入和快取
  • 實作自動註冊和序號產生器制

在本章中,我們將探討更多描述器的應用範例,並學習如何有效地使用描述器來簡化程式碼和提高可維護性。

描述器(Descriptor)協定詳解:掌握 Python 進階屬性的關鍵

描述器是 Python 中一個強大的工具,允許開發者以透明和封裝的方式實作屬性的複雜邏輯控制。在本章中,我們將探討描述器協定的各個方法,包括 __get____set__ 等,並透過例項展示如何利用這些方法建立更為抽象和通用的屬性管理機制。

探索描述器協定的每個方法

描述器協定主要由三個特殊方法組成:__get____set____delete__。這些方法使得描述器能夠與類別屬性的存取、修改和刪除操作進行互動。

__get__ 方法

__get__(self, instance, owner) 方法是描述器協定中最重要的方法之一,用於控制屬性的讀取行為。其中,instance 引數代表呼叫描述器的例項,而 owner 則是該例項所屬的類別。

class DescriptorClass:
    def __get__(self, instance, owner):
        if instance is None:
            return f"{self.__class__.__name__}.{owner.__name__}"
        return f"value for {instance}"

class ClientClass:
    descriptor = DescriptorClass()

內容解密:

  1. 當直接透過類別存取描述器屬性時(例如 ClientClass.descriptor),instanceNone,此時傳回描述器的類別名稱和所有者類別名稱的組合字串。
  2. 當透過例項存取描述器屬性時(例如 ClientClass().descriptor),傳回一個與例項相關的字串。
  3. 這種設計允許開發者根據不同的呼叫情境執行不同的邏輯。

__set__ 方法

__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 Field:
    def __init__(self, *validations):
        self._name = None
        self.validations = validations

    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 validate(self, value):
        for validation in self.validations:
            validation(value)

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

class ClientClass:
    descriptor = Field(
        Validation(lambda x: isinstance(x, (int, float)), "is not a number"),
        Validation(lambda x: x >= 0, "is negative")
    )

內容解密:

  1. Field 類別利用 Validation 類別實作了對屬性值的驗證邏輯。
  2. __set__ 方法中,首先呼叫 validate 方法檢查指定是否合法。
  3. 如果驗證透過,則將值儲存在例項的 __dict__ 中,避免覆寫描述器本身。
  4. 這種機制允許開發者定義可重用的驗證邏輯,並靈活應用於不同的屬性。

描述器的優勢與應用場景

描述器提供了一種高度靈活和可擴充套件的方式來管理屬性的存取和修改行為。透過結合 __get____set__ 方法,開發者可以實作諸如資料驗證、延遲載入、日誌記錄等複雜邏輯,而無需修改類別的其他部分。