Python 中的裝飾器(Decorators) 是一種強(qiáng)大且優(yōu)雅的語法糖,它允許你在不修改原始函數(shù)或類定義的情況下,動(dòng)態(tài)地給它們添加功能或修改它們的行為。你可以將裝飾器想象成一個(gè)“包裝器”,它在不改變被包裝對(duì)象內(nèi)容的前提下,為其增加新的特性。
什么是裝飾器?
從本質(zhì)上講,裝飾器是一個(gè)接受函數(shù)(或類)作為輸入,并返回一個(gè)新函數(shù)(或新類)的可調(diào)用對(duì)象(callable)。這個(gè)新函數(shù)/類通常會(huì)包含原始函數(shù)/類的邏輯,并在此基礎(chǔ)上添加一些額外的功能。
Python 中使用 @ 符號(hào)來應(yīng)用裝飾器,它放在函數(shù)或類定義的上方。
# 定義一個(gè)裝飾器函數(shù)
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func() # 調(diào)用原始函數(shù)
print("Something is happening after the function is called.")
return wrapper
@my_decorator # 使用 @ 語法糖應(yīng)用裝飾器
def say_hello():
print("Hello!")
say_hello()
輸出:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
在這個(gè)例子中,my_decorator 就是一個(gè)裝飾器。@my_decorator 放在 say_hello 函數(shù)上方,等同于執(zhí)行了 say_hello = my_decorator(say_hello)。
裝飾器的工作原理
- 定義裝飾器:你首先定義一個(gè)函數(shù)(或類,更高級(jí)的用法),它接受一個(gè)函數(shù)作為參數(shù)。
-
定義內(nèi)部函數(shù)(Wrapper):在這個(gè)裝飾器內(nèi)部,你通常會(huì)定義一個(gè)嵌套函數(shù)(通常命名為
wrapper),這個(gè)wrapper函數(shù)會(huì)包含你想要添加的額外邏輯,并且會(huì)在某個(gè)地方調(diào)用原始函數(shù)。 -
返回 Wrapper:裝飾器函數(shù)最后返回這個(gè)
wrapper函數(shù)。 -
應(yīng)用裝飾器:當(dāng)你使用
@decorator_name語法時(shí),Python 會(huì)自動(dòng)將被裝飾的函數(shù)作為參數(shù)傳遞給decorator_name函數(shù),然后用返回的wrapper函數(shù)替換掉原始函數(shù)。
為什么使用裝飾器?
裝飾器提供了一種優(yōu)雅的方式來實(shí)現(xiàn)以下目標(biāo):
- 代碼重用:將通用的功能(如日志、性能計(jì)時(shí)、權(quán)限檢查、緩存等)封裝成裝飾器,可以在多個(gè)函數(shù)或類中重復(fù)使用,避免代碼重復(fù)。
- 關(guān)注點(diǎn)分離:將核心業(yè)務(wù)邏輯與非核心的橫切關(guān)注點(diǎn)(cross-cutting concerns)分離。例如,一個(gè)函數(shù)只負(fù)責(zé)其核心計(jì)算,而日志記錄則由裝飾器處理。
-
提高可讀性:通過簡潔的
@語法,代碼的意圖更加清晰。 - 不修改原始代碼:你可以在不觸碰原始函數(shù)或類定義的情況下,擴(kuò)展或修改其行為。
裝飾器的常見應(yīng)用場(chǎng)景
- 日志記錄(Logging):記錄函數(shù)調(diào)用的時(shí)間、參數(shù)和返回值。
- 性能分析(Profiling):測(cè)量函數(shù)的執(zhí)行時(shí)間。
- 權(quán)限檢查(Authorization):驗(yàn)證用戶是否有權(quán)訪問某個(gè)函數(shù)或資源。
- 緩存(Caching):緩存函數(shù)的返回結(jié)果,避免重復(fù)計(jì)算。
- 輸入驗(yàn)證(Input Validation):在函數(shù)執(zhí)行前檢查輸入?yún)?shù)的有效性。
- 重試機(jī)制(Retries):當(dāng)函數(shù)失敗時(shí)自動(dòng)重試。
- API 路由(API Routing):在 Web 框架中,如 Flask 和 Django,裝飾器常用于將 URL 路徑映射到視圖函數(shù)。
帶參數(shù)的函數(shù)裝飾器
如果被裝飾的函數(shù)需要參數(shù),你的 wrapper 函數(shù)也必須能接收這些參數(shù),并傳遞給原始函數(shù)。這通常通過 *args 和 **kwargs 來實(shí)現(xiàn)。
def log_arguments(func):
def wrapper(*args, **kwargs): # 接收任意位置參數(shù)和關(guān)鍵字參數(shù)
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_arguments
def add(a, b):
return a + b
@log_arguments
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
add(10, 20)
greet("Alice", greeting="Hi")
帶參數(shù)的裝飾器
有些時(shí)候,你可能需要自定義裝飾器的行為,這就需要裝飾器本身接收參數(shù)。這通常通過一個(gè)三層嵌套的結(jié)構(gòu)來實(shí)現(xiàn)。外層函數(shù)接收裝飾器參數(shù),然后返回真正的裝飾器函數(shù)。
def repeat(num_times): # 第一層:接收裝飾器參數(shù)
def decorator(func): # 第二層:接收被裝飾的函數(shù)
def wrapper(*args, **kwargs): # 第三層:接收被裝飾函數(shù)的參數(shù)
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(num_times=3) # 調(diào)用裝飾器,傳入?yún)?shù)
def bark():
print("Woof!")
@repeat(num_times=2)
def greet_person(name):
print(f"Hello, {name}!")
bark()
print("---")
greet_person("Bob")
類裝飾器
裝飾器不僅可以裝飾函數(shù),還可以裝飾類。此外,你也可以使用類來作為裝飾器,如果這個(gè)類實(shí)現(xiàn)了 __call__ 方法。
# 類作為裝飾器
class CountCalls:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs): # 使類的實(shí)例可調(diào)用
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("Hi!")
say_hi()
say_hi()
@functools.wraps
在使用裝飾器時(shí),一個(gè)常見的問題是,裝飾器會(huì)替換掉原始函數(shù)的元數(shù)據(jù)(如 __name__、__doc__)。為了解決這個(gè)問題,Python 提供了 functools.wraps 裝飾器。
import functools
def my_logging_decorator(func):
@functools.wraps(func) # 使用 wraps 裝飾器
def wrapper(*args, **kwargs):
"""This is the wrapper function's docstring."""
print(f"Logging call to {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_logging_decorator
def my_function():
"""This is my_function's docstring."""
pass
print(my_function.__name__) # 輸出: my_function
print(my_function.__doc__) # 輸出: This is my_function's docstring.
如果沒有 @functools.wraps(func),my_function.__name__ 會(huì)是 wrapper,my_function.__doc__ 會(huì)是 wrapper 函數(shù)的文檔字符串。
裝飾器是 Python 中一個(gè)非常強(qiáng)大且常用的高級(jí)特性,理解它對(duì)于編寫簡潔、可維護(hù)和可擴(kuò)展的代碼至關(guān)重要。
你對(duì)裝飾器還有什么具體的問題嗎?