Python 的裝飾器提供不修改原始碼即可擴充套件函式行為的機制,但使用時需注意副作用的產生。functools.wraps 能保留原始函式的後設資料,避免副作用在匯入模組時就被執行。裝飾器的設計應具備通用性,能處理不同型別的物件,例如函式、類別和方法。此外,副作用相關程式碼應置於內部函式,確保在函式被呼叫時才執行。

在軟體設計中,裝飾器可以有效地實作關注點分離,例如將日誌記錄、效能計時等橫切關注點與核心業務邏輯分離。同時,裝飾器也能夠促進 DRY 原則的應用,避免重複的程式碼邏輯。然而,並非所有情況都適合使用裝飾器,應謹慎評估其使用場景,並遵循最佳實踐,例如使用有意義的名稱、確保程式碼乾淨可維護,以及優先考慮組合而非繼承。

裝飾器(Decorator)中的副作用處理與最佳實踐

在Python中,裝飾器是一種強大的工具,能夠在不修改原始函式程式碼的情況下,擴充套件或修改其行為。然而,在使用裝飾器時,我們需要注意避免在裝飾器的外部產生副作用(Side Effects),因為這可能會導致一些意想不到的問題。

使用functools.wraps保留原始函式的後設資料

當我們建立一個裝飾器時,原始函式的後設資料(如__doc____name__等)會被裝飾器函式的後設資料所覆寫。這可能會導致一些問題,例如使用doctest模組測試函式時,測試無法正常執行。

為瞭解決這個問題,我們可以使用functools.wraps裝飾器來保留原始函式的後設資料。以下是一個範例:

# decorator_wraps_2.py
from functools import wraps

def trace_decorator(function):
    @wraps(function)
    def wrapped(*args, **kwargs):
        logger.info("running %s", function.__qualname__)
        return function(*args, **kwargs)
    return wrapped

內容解密:

  1. @wraps(function):使用functools.wraps裝飾器來保留原始函式function的後設資料。
  2. wrapped函式:定義了一個內部函式wrapped,它會在呼叫原始函式之前記錄一些資訊。
  3. logger.info:記錄原始函式的呼叫資訊。
  4. return function(*args, **kwargs):呼叫原始函式並傳回其結果。

透過使用functools.wraps,我們可以確保原始函式的後設資料不會被裝飾器所覆寫。

裝飾器中的副作用處理

在裝飾器中,我們應該避免在外部產生副作用。副作用是指在裝飾器外部執行的程式碼,它可能會在匯入模組時被執行,而不是在函式被呼叫時。

以下是一個錯誤的範例:

# decorator_side_effects_1.py
def traced_function_wrong(function):
    logger.info("started execution of %s", function)
    start_time = time.time()
    @wraps(function)
    def wrapped(*args, **kwargs):
        result = function(*args, **kwargs)
        logger.info(
            "function %s took %.2fs",
            function,
            time.time() - start_time
        )
        return result
    return wrapped

內容解密:

  1. logger.info("started execution of %s", function):在裝飾器外部記錄了一條資訊,這是一個副作用。
  2. start_time = time.time():在裝飾器外部記錄了當前時間,這也是一個副作用。

這個範例的問題在於,當我們匯入這個模組時,裝飾器的外部程式碼會被執行,而不是在函式被呼叫時。這可能會導致一些意想不到的問題。

正確的處理方式

為瞭解決這個問題,我們應該將副作用相關的程式碼移到內部函式中:

# decorator_side_effects_2.py
def traced_function(function):
    @functools.wraps(function)
    def wrapped(*args, **kwargs):
        logger.info("started execution of %s", function.__qualname__)
        start_time = time.time()
        result = function(*args, **kwargs)
        logger.info(
            "function %s took %.2fs",
            function.__qualname__,
            time.time() - start_time
        )
        return result
    return wrapped

內容解密:

  1. logger.info("started execution of %s", function.__qualname__):將記錄資訊的程式碼移到內部函式中。
  2. start_time = time.time():將記錄當前時間的程式碼移到內部函式中。

透過這種方式,我們可以確保副作用只會在函式被呼叫時發生,而不是在匯入模組時。

深入解析裝飾器(Decorator)的應用與挑戰

裝飾器是Python中一個強大的工具,能夠在不修改原始函式或類別的情況下擴充套件其功能。然而,在實際應用中,裝飾器的使用也伴隨著一些挑戰和限制。

裝飾器的副作用

裝飾器的執行可能伴隨著副作用,例如註冊物件到公共登入檔中。以下是一個例子,展示瞭如何使用裝飾器將事件類別註冊到EVENTS_REGISTRY中:

EVENTS_REGISTRY = {}

def register_event(event_cls):
    """將事件類別註冊到登入檔中"""
    EVENTS_REGISTRY[event_cls.__name__] = event_cls
    return event_cls

@register_event
class UserLoginEvent:
    """代表使用者登入事件"""
    TYPE = "user"

@register_event
class UserLogoutEvent:
    """代表使用者登出事件"""
    TYPE = "user"

內容解密:

  • EVENTS_REGISTRY是一個用於存放已註冊事件類別的字典。
  • register_event裝飾器將事件類別註冊到EVENTS_REGISTRY中。
  • @register_event語法糖用於簡化註冊過程,使程式碼更具可讀性。

裝飾器的通用性設計

為了使裝飾器能夠適用於不同的物件(例如函式、類別、方法或靜態方法),我們需要進行通用性設計。以下是一個例子,展示瞭如何設計一個能夠將資料函式庫連線字串轉換為DBDriver物件的裝飾器:

from functools import wraps

class DBDriver:
    def __init__(self, dbstring: str) -> None:
        self.dbstring = dbstring

    def execute(self, query: str) -> str:
        return f"query {query} at {self.dbstring}"

def inject_db_driver(function):
    """將資料函式庫連線字串轉換為DBDriver物件"""
    @wraps(function)
    def wrapped(dbstring):
        return function(DBDriver(dbstring))
    return wrapped

@inject_db_driver
def run_query(driver):
    return driver.execute("test_function")

內容解密:

  • DBDriver類別代表資料函式庫驅動程式,能夠執行查詢操作。
  • inject_db_driver裝飾器將資料函式庫連線字串轉換為DBDriver物件,並將其傳遞給被裝飾的函式。
  • @inject_db_driver語法糖簡化了轉換過程,使程式碼更具可讀性。

使用裝飾器最佳化軟體設計

在前面的章節中,我們已經探討了裝飾器的基本概念、實作方式以及常見問題的解決方案。現在,我們將進一步探討如何利用裝飾器來實作更好的軟體設計。

組合優於繼承

在物件導向程式設計中,繼承是一種常見的程式碼重用方式。然而,繼承也可能導致類別之間的緊密耦合,從而使得程式碼更難以維護。因此,我們通常建議使用組合(composition)而不是繼承。

在《Design Patterns: Elements of Reusable Object-Oriented Software》(DESIG01)一書中,大多數設計模式都是根據組合優於繼承的理念。

動態解析屬性

假設我們正在與一個框架互動,該框架要求我們實作一個名為 resolve_<屬性名稱> 的方法來解析屬性。然而,我們的領域物件只有沒有 resolve_ 字首的屬性。

一種解決方案是使用 __getattr__ 魔法方法來動態解析屬性。我們可以將其放在基礎類別中:

class BaseResolverMixin:
    def __getattr__(self, attr: str):
        if attr.startswith("resolve_"):
            *_, actual_attr = attr.partition("resolve_")
        else:
            actual_attr = attr
        try:
            return self.__dict__[actual_attr]
        except KeyError as e:
            raise AttributeError from e

@dataclass
class Customer(BaseResolverMixin):
    customer_id: str
    name: str
    address: str

然而,我們可以使用類別裝飾器來直接設定這個方法:

from dataclasses import dataclass

def _resolver_method(self, attr):
    """解析屬性的自訂方法"""
    if attr.startswith("resolve_"):
        *_, actual_attr = attr.partition("resolve_")
    else:
        actual_attr = attr
    try:
        return self.__dict__[actual_attr]
    except KeyError as e:
        raise AttributeError from e

def with_resolver(cls):
    """設定類別的自訂解析方法"""
    cls.__getattr__ = _resolver_method
    return cls

@dataclass
@with_resolver
class Customer:
    customer_id: str
    name: str
    address: str

兩種實作方式都可以滿足以下行為:

>>> customer = Customer("1", "name", "address")
>>> customer.resolve_customer_id
'1'
>>> customer.resolve_name
'name'

組合的優勢

使用類別裝飾器的實作方式比使用繼承的實作方式更好。首先,我們使用組合(將類別修改並傳回新的類別)而不是繼承,因此程式碼的耦合度更低。

此外,使用繼承的方式在概念上是有問題的。我們不是使用繼承來建立更專門的類別,而是僅僅為了重用 __getattr__ 方法。這不是繼承的正確用法。

裝飾器的最佳實踐

在使用裝飾器時,我們應該遵循以下最佳實踐:

  • 使用有意義的名稱和註解來描述裝飾器的功能。
  • 確保裝飾器的實作是乾淨和可維護的。
  • 使用組合而不是繼承來重用程式碼。

實作一個資料函式庫驅動程式注入裝飾器

以下是一個實作資料函式庫驅動程式注入裝飾器的範例:

from functools import wraps
from types import MethodType

class inject_db_driver:
    """將字串轉換為DBDriver例項並傳遞給被包裝函式"""
    def __init__(self, function) -> None:
        self.function = function
        wraps(self.function)(self)

    def __call__(self, dbstring):
        return self.function(DBDriver(dbstring))

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.__class__(MethodType(self.function, instance))

這個裝飾器可以用於函式或方法,並且可以正確地處理 self 引數。

深入理解裝飾器在軟體設計中的應用

在軟體開發中,裝飾器是一種強大的工具,能夠幫助開發者以簡潔的方式實作程式碼的重用和功能的擴充套件。裝飾器模式的核心思想是透過包裝原有的函式或類別,新增新的功能而不改變原有的程式碼結構。

裝飾器的正確認知

首先,我們需要正確理解裝飾器的使用場景。裝飾器通常用於實作某些與主要業務邏輯無關的功能,例如日誌記錄、錯誤處理、快取等。以 ConnectionEncryptedConnection 為例,後者繼承前者是合理的,因為加密連線是一種特殊的連線。然而,對於Mixin類別,如 BaseResolverMixin,其設計初衷是為了提供某種特定的功能,這種情況下需要謹慎評估是否真正需要Mixin。

裝飾器的可擴充套件性與DRY原則

裝飾器的一個重要優點是其可擴充套件性。透過引數化裝飾器,我們可以實作更大的靈活性,使其能夠適用於不同的場景。例如,在事件處理中定義一個類別裝飾器,能夠統一管理事件的序列化邏輯,避免在多個類別中重複相同的程式碼。這符合DRY(Don’t Repeat Yourself)原則,即避免重複的程式碼邏輯。

程式碼範例:DRY原則的實踐

def singleton(cls):
    instances = dict()
    def wrap(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrap

@singleton
class Logger:
    def __init__(self):
        pass

    def log(self, message):
        print(f"Logging: {message}")

#### 內容解密:
此範例展示瞭如何使用裝飾器實作單例模式
1. `singleton` 函式是一個裝飾器工廠傳回 `wrap` 函式
2. `wrap` 函式負責檢查 `Logger` 類別是否已經被例項化若未例項化則建立例項並儲存在 `instances` 字典中
3. 這樣無論呼叫多少次 `Logger()`,都只會傳回同一個例項實作了單例模式
4. 使用 `@singleton` 裝飾 `Logger` 類別使得 `Logger` 成為單例類別

何時使用裝飾器

雖然裝飾器能夠帶來程式碼重用的好處,但並非所有情況下都適合使用裝飾器。一般來說,只有當某個功能需要在多個地方重複使用時,才考慮將其抽象成裝飾器。此外,在決定是否使用裝飾器之前,應至少有三次以上的重複使用場景。

裝飾器與關注點分離

一個好的裝飾器應該只負責一件事情,遵循單一責任原則(SRP)。如果一個裝飾器承擔了多個責任,那麼它應該被分解成多個更小的裝飾器,每個裝飾器負責一個特定的功能。例如,可以將日誌記錄和計時功能分成兩個不同的裝飾器:log_executionmeasure_time

程式碼範例:關注點分離

import functools
import time
import logging

logger = logging.getLogger(__name__)

def log_execution(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        logger.info("started execution of %s", func.__qualname__)
        return func(*args, **kwargs)
    return wrapped

def measure_time(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        logger.info("function %s took %.2fs", func.__qualname__, time.time() - start_time)
        return result
    return wrapped

@measure_time
@log_execution
def example_function():
    time.sleep(1)  # 模擬耗時操作

#### 內容解密:
此範例展示瞭如何將日誌記錄和計時功能分離到不同的裝飾器中
1. `log_execution` 裝飾器負責記錄函式的執行開始日誌
2. `measure_time` 裝飾器負責記錄函式的執行時間
3. 透過組合使用 `@measure_time``@log_execution`,可以在保持程式碼簡潔的同時實作多個功能
4. 使用 `functools.wraps` 保留原函式的元資訊如名稱和檔案字串