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
內容解密:
@wraps(function):使用functools.wraps裝飾器來保留原始函式function的後設資料。wrapped函式:定義了一個內部函式wrapped,它會在呼叫原始函式之前記錄一些資訊。logger.info:記錄原始函式的呼叫資訊。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
內容解密:
logger.info("started execution of %s", function):在裝飾器外部記錄了一條資訊,這是一個副作用。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
內容解密:
logger.info("started execution of %s", function.__qualname__):將記錄資訊的程式碼移到內部函式中。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 引數。
深入理解裝飾器在軟體設計中的應用
在軟體開發中,裝飾器是一種強大的工具,能夠幫助開發者以簡潔的方式實作程式碼的重用和功能的擴充套件。裝飾器模式的核心思想是透過包裝原有的函式或類別,新增新的功能而不改變原有的程式碼結構。
裝飾器的正確認知
首先,我們需要正確理解裝飾器的使用場景。裝飾器通常用於實作某些與主要業務邏輯無關的功能,例如日誌記錄、錯誤處理、快取等。以 Connection 和 EncryptedConnection 為例,後者繼承前者是合理的,因為加密連線是一種特殊的連線。然而,對於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_execution 和 measure_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` 保留原函式的元資訊,如名稱和檔案字串。