Python裝飾器decorator實(shí)際應(yīng)用與高級(jí)使用詳解
在這篇文章中,我們來詳細(xì)、系統(tǒng)地講解一下Python的裝飾器(Decorator)。我會(huì)從基本概念講起,循序漸進(jìn),直到實(shí)際應(yīng)用和高級(jí)用法。
1. 什么是裝飾器?(核心思想)
裝飾器本質(zhì)上是一個(gè)Python函數(shù),它可以讓其他函數(shù)在不改變其源代碼的情況下增加額外的功能。
核心思想:包裝 (Wrapping)。
你可以把裝飾器想象成一個(gè)包裝禮物的過程:
- 你的函數(shù) = 禮物本身
- 裝飾器 = 包裝紙、彩帶和蝴蝶結(jié)
禮物(函數(shù))的核心功能沒有變,但經(jīng)過包裝(裝飾器)后,它看起來更漂亮、更完整了(增加了新功能)。
在代碼層面,裝飾器是一個(gè)接收函數(shù)作為參數(shù),并返回一個(gè)新函數(shù)的函數(shù)。
2. 為什么需要裝飾器?(動(dòng)機(jī))
假設(shè)我們有很多函數(shù),現(xiàn)在想給每個(gè)函數(shù)都加上一個(gè)“計(jì)算運(yùn)行時(shí)間”的功能。
不好的方法:修改每個(gè)函數(shù)
import time
def func_A():
start_time = time.time()
print("函數(shù)A開始執(zhí)行...")
time.sleep(1)
print("函數(shù)A執(zhí)行完畢。")
end_time = time.time()
print(f"函數(shù)A耗時(shí): {end_time - start_time:.2f}秒")
def func_B():
start_time = time.time()
print("函數(shù)B開始執(zhí)行...")
time.sleep(2)
print("函數(shù)B執(zhí)行完畢。")
end_time = time.time()
print(f"函數(shù)B耗時(shí): {end_time - start_time:.2f}秒")
# ... 還有 func_C, func_D ...
問題:
- 代碼冗余:計(jì)時(shí)邏輯在每個(gè)函數(shù)里都重復(fù)了一遍。
- 違反“開放封閉原則”:每次新增需求(比如再加個(gè)日志功能),都要去修改已有函數(shù)的代碼。
裝飾器就是為了解決這類問題而生的,它能將這些通用的、與核心業(yè)務(wù)無關(guān)的功能(如計(jì)時(shí)、日志、認(rèn)證)抽離出來。
3. Python基礎(chǔ):理解裝飾器的前提
要理解裝飾器,必須先理解Python中 “函數(shù)是一等公民” (Functions are First-Class Objects) 的概念。這意味著函數(shù)可以:
被賦值給一個(gè)變量
def say_hello():
print("Hello!")
greet = say_hello # 將函數(shù)say_hello賦值給變量greet
greet() # 輸出: Hello!
作為參數(shù)傳遞給另一個(gè)函數(shù)
def do_something(func):
print("準(zhǔn)備做某事...")
func() # 調(diào)用傳入的函數(shù)
print("做完了。")
def work():
print("正在努力工作...")
do_something(work)
# 輸出:
# 準(zhǔn)備做某事...
# 正在努力工作...
# 做完了。
在函數(shù)內(nèi)部定義,并作為返回值返回
def get_greeter():
def greet():
print("你好,世界!")
return greet # 返回內(nèi)部定義的greet函數(shù)
greeter_func = get_greeter()
greeter_func() # 輸出: 你好,世界!
裝飾器正是利用了這幾點(diǎn)特性,尤其是第2點(diǎn)和第3點(diǎn)。
4. 一步步構(gòu)建你的第一個(gè)裝飾器
最簡(jiǎn)單的裝飾器(手動(dòng)方式)
我們來寫一個(gè)簡(jiǎn)單的裝飾器,它在函數(shù)執(zhí)行前后打印信息。
# 1. 定義一個(gè)裝飾器函數(shù)
def my_decorator(func):
# 2. 定義一個(gè)內(nèi)部包裝函數(shù),它將“包裝”原始函數(shù)
def wrapper():
print("在被裝飾的函數(shù)執(zhí)行之前...")
func() # 3. 調(diào)用原始函數(shù)
print("在被裝飾的函數(shù)執(zhí)行之后...")
# 4. 返回這個(gè)包裝好的函數(shù)
return wrapper
# 定義一個(gè)需要被裝飾的函數(shù)
def say_whee():
print("Whee!")
# 手動(dòng)使用裝飾器
say_whee = my_decorator(say_whee)
# 現(xiàn)在調(diào)用say_whee,實(shí)際上是在調(diào)用wrapper函數(shù)
say_whee()
輸出:
在被裝飾的函數(shù)執(zhí)行之前... Whee! 在被裝飾的函數(shù)執(zhí)行之后...
say_whee = my_decorator(say_whee) 這行代碼是理解裝飾器的關(guān)鍵。
它用 my_decorator 返回的 wrapper 函數(shù)覆蓋了原來的 say_whee 函數(shù)。
使用@語法糖
Python提供了一個(gè)更優(yōu)雅、更Pythonic的方式來使用裝飾器,那就是 @ 語法糖。
def my_decorator(func):
def wrapper():
print("在被裝飾的函數(shù)執(zhí)行之前...")
func()
print("在被裝飾的函數(shù)執(zhí)行之后...")
return wrapper
@my_decorator # 這行等價(jià)于 say_whee = my_decorator(say_whee)
def say_whee():
print("Whee!")
say_whee()
輸出和上面完全一樣,但代碼是不是簡(jiǎn)潔多了?
5. 處理帶參數(shù)的函數(shù)
如果我們的原始函數(shù)帶參數(shù)怎么辦?比如 greet(name)。上面的 wrapper 函數(shù)不接受任何參數(shù),會(huì)報(bào)錯(cuò)。
我們需要讓 wrapper 函數(shù)能接受任意參數(shù),并把它們傳遞給原始函數(shù)。這里就要用到 *args 和 **kwargs。
def decorator_with_args(func):
# wrapper現(xiàn)在可以接受任意數(shù)量的位置參數(shù)和關(guān)鍵字參數(shù)
def wrapper(*args, **kwargs):
print("裝飾器:函數(shù)開始執(zhí)行")
result = func(*args, **kwargs) # 將參數(shù)傳遞給原始函數(shù),并保存返回值
print("裝飾器:函數(shù)執(zhí)行完畢")
return result # 返回原始函數(shù)的執(zhí)行結(jié)果
return wrapper
@decorator_with_args
def greet(name, message="你好"):
print(f"{message}, {name}!")
return f"問候了 {name}"
returned_value = greet("Alice", message="早上好")
print(f"函數(shù)返回值: {returned_value}")
輸出:
裝飾器:函數(shù)開始執(zhí)行 早上好, Alice! 裝飾器:函數(shù)執(zhí)行完畢 函數(shù)返回值: 問候了 Alice
6.functools.wraps的重要性
使用裝飾器有一個(gè)小問題:它會(huì)丟失原始函數(shù)的一些元信息(metadata),比如函數(shù)名 (__name__)、文檔字符串 (__doc__)等。
@decorator_with_args
def greet(name):
"""這是一個(gè)打招呼的函數(shù)"""
print(f"你好, {name}!")
print(greet.__name__) # 輸出: wrapper (而不是 greet)
print(greet.__doc__) # 輸出: None (而不是 "這是一個(gè)打招呼的函數(shù)")
這對(duì)于調(diào)試和自省工具來說非常不便。為了解決這個(gè)問題,Python的 functools 模塊提供了一個(gè)專門的裝飾器:@wraps。
import functools
def decorator_for_wraps(func):
@functools.wraps(func) # 關(guān)鍵!將func的元信息拷貝到wrapper上
def wrapper(*args, **kwargs):
# ... 裝飾器邏輯 ...
print("裝飾器邏輯...")
return func(*args, **kwargs)
return wrapper
@decorator_for_wraps
def greet(name):
"""這是一個(gè)打招呼的函數(shù)"""
print(f"你好, {name}!")
print(greet.__name__) # 輸出: greet
print(greet.__doc__) # 輸出: 這是一個(gè)打招呼的函數(shù)
最佳實(shí)踐: 編寫任何裝飾器時(shí),都應(yīng)該在你的 wrapper 函數(shù)上使用 @functools.wraps。
7. 帶參數(shù)的裝飾器(進(jìn)階)
如果我們想讓裝飾器本身也接收參數(shù)呢?比如 @repeat(num=3),讓函數(shù)重復(fù)執(zhí)行3次。
這就需要再加一層函數(shù)嵌套。
結(jié)構(gòu):
- 最外層函數(shù) (
repeat):接收裝飾器的參數(shù)(如num=3),并返回一個(gè)真正的裝飾器。 - 中間層函數(shù) (
decorator_repeat):這就是我們之前寫的標(biāo)準(zhǔn)裝飾器,它接收一個(gè)函數(shù)作為參數(shù)。 - 最內(nèi)層函數(shù) (
wrapper):執(zhí)行增強(qiáng)后的邏輯。
import functools
def repeat(num=2): # 1. 最外層函數(shù),接收裝飾器參數(shù)
def decorator_repeat(func): # 2. 中間層,接收被裝飾的函數(shù)
@functools.wraps(func)
def wrapper(*args, **kwargs): # 3. 最內(nèi)層,執(zhí)行邏輯
print(f"函數(shù)將重復(fù)執(zhí)行 {num} 次。")
for _ in range(num):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num=4)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
執(zhí)行過程:
@repeat(num=4)首先被調(diào)用,它返回decorator_repeat函數(shù)。- Python接著執(zhí)行
@decorator_repeat,這等價(jià)于say_hello = decorator_repeat(say_hello)。 - 最終
say_hello變量指向了wrapper函數(shù)。
8. 類裝飾器(進(jìn)階)
除了用函數(shù),我們也可以用類來創(chuàng)建裝飾器。這在需要維護(hù)狀態(tài)時(shí)特別有用。一個(gè)類要成為裝飾器,需要實(shí)現(xiàn) __init__ 和 __call__ 方法。
class Counter:
def __init__(self, func):
functools.update_wrapper(self, func) # 類似 @wraps
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"'{self.func.__name__}' 已被調(diào)用 {self.num_calls} 次。")
return self.func(*args, **kwargs)
@Counter
def some_function():
print("執(zhí)行 some_function")
some_function()
some_function()
some_function()
輸出:
'some_function' 已被調(diào)用 1 次。 執(zhí)行 some_function 'some_function' 已被調(diào)用 2 次。 執(zhí)行 some_function 'some_function' 已被調(diào)用 3 次。 執(zhí)行 some_function
Counter 類的實(shí)例 some_function 能夠記住自己被調(diào)用的次數(shù)。
9. 裝飾器的實(shí)際應(yīng)用場(chǎng)景(重點(diǎn))
日志記錄 (Logging)
import functools
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def log_function_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling function '{func.__name__}' with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"Function '{func.__name__}' returned {result}")
return result
return wrapper
@log_function_call
def add(a, b):
return a + b
add(5, 3)
性能計(jì)時(shí) (Timing)
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Function '{func.__name__}' took {run_time:.4f} seconds to complete.")
return result
return wrapper
@timer
def process_data():
time.sleep(1)
print("數(shù)據(jù)處理完成!")
process_data()
用戶認(rèn)證 (Authentication)
在Web框架(如Flask, Django)中非常常見。
# 這是一個(gè)簡(jiǎn)化的概念性例子
import functools
def login_required(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 偽代碼:假設(shè)有一個(gè)函數(shù) check_user_logged_in()
if check_user_logged_in():
return func(*args, **kwargs)
else:
raise PermissionError("用戶未登錄,禁止訪問!")
return wrapper
# 模擬一個(gè)全局登錄狀態(tài)
_user_is_logged_in = False
def check_user_logged_in(): return _user_is_logged_in
@login_required
def view_profile():
print("這是用戶的個(gè)人資料頁(yè)面。")
# 嘗試調(diào)用
try:
view_profile()
except PermissionError as e:
print(e) # 輸出: 用戶未登錄,禁止訪問!
# 模擬用戶登錄
_user_is_logged_in = True
print("\n用戶登錄后:")
view_profile() # 輸出: 這是用戶的個(gè)人資料頁(yè)面。
緩存 (Caching/Memoization)
對(duì)于計(jì)算成本高的函數(shù),可以將結(jié)果緩存起來。Python 3.9+ 自帶了 functools.cache。我們也可以自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的版本。
import functools
import time
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
# 使用Python自帶的會(huì)更高效
# from functools import lru_cache, cache
@memoize # 或者 @functools.cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
start = time.time()
print(fibonacci(35))
end = time.time()
print(f"第一次計(jì)算耗時(shí): {end - start:.4f}s")
start = time.time()
print(fibonacci(35))
end = time.time()
print(f"第二次(從緩存)計(jì)算耗時(shí): {end - start:.4f}s")
輸入驗(yàn)證 (Validation)
在函數(shù)執(zhí)行前檢查其參數(shù)是否符合要求。
import functools
def validate_types(*type_args):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i, (arg, expected_type) in enumerate(zip(args, type_args)):
if not isinstance(arg, expected_type):
raise TypeError(f"Argument {i+1} must be of type {expected_type.__name__}, not {type(arg).__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(int, int)
def multiply(a, b):
return a * b
print(multiply(5, 10)) # 正常工作
try:
multiply(5, "10") # 會(huì)拋出 TypeError
except TypeError as e:
print(e)
10. 裝飾器棧(多個(gè)裝飾器)
一個(gè)函數(shù)可以被多個(gè)裝飾器修飾。執(zhí)行順序是從下到上(最靠近函數(shù)的先被應(yīng)用),然后執(zhí)行時(shí)是從上到下。
@timer
@log_function_call
def complex_calculation(x, y):
time.sleep(0.5)
return x * y + 2
這等價(jià)于:
complex_calculation = timer(log_function_call(complex_calculation))
執(zhí)行 complex_calculation(3, 4) 時(shí):
timer的wrapper開始執(zhí)行,記錄開始時(shí)間。timer調(diào)用log_function_call的wrapper。log_function_call的wrapper開始執(zhí)行,打印日志 “Calling function…”。log_function_call調(diào)用原始的complex_calculation函數(shù)。complex_calculation執(zhí)行,返回結(jié)果14。log_function_call的wrapper拿到結(jié)果,打印日志 “…returned 14”。timer的wrapper拿到結(jié)果,記錄結(jié)束時(shí)間,并打印耗時(shí)。
11. 總結(jié)
- 核心:裝飾器是一個(gè)接收函數(shù)并返回新函數(shù)的函數(shù),用于在不修改原函數(shù)代碼的情況下增加功能。
- 基礎(chǔ):依賴于Python中函數(shù)是“一等公民”的特性。
- 語法糖:
@decorator是my_func = decorator(my_func)的簡(jiǎn)寫。 - 通用性:使用
*args, **kwargs來處理任意參數(shù)的函數(shù)。 - 最佳實(shí)踐:始終使用
@functools.wraps來保留原函數(shù)的元信息。 - 靈活性:裝飾器可以帶參數(shù),也可以用類來實(shí)現(xiàn)。
- 強(qiáng)大應(yīng)用:日志、計(jì)時(shí)、認(rèn)證、緩存、驗(yàn)證等都是裝飾器的經(jīng)典用例,極大地提高了代碼的復(fù)用性和可維護(hù)性。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
python學(xué)習(xí)筆記之列表(list)與元組(tuple)詳解
List(列表)是Python中使用最頻繁的數(shù)據(jù)類型,而元組是另一個(gè)數(shù)據(jù)類型,類似于List(列表)。這篇文章主要給大家介紹了python學(xué)習(xí)筆記之列表(list)與元組(tuple)的相關(guān)資料,文中不僅詳細(xì)介紹了兩者的使用,還分享了list和tuple的相互轉(zhuǎn)換的方法,需要的朋友可以參考下2017-11-11
python函數(shù)默認(rèn)參數(shù)使用避坑指南
這篇文章主要為大家介紹了python函數(shù)默認(rèn)參數(shù)使用的踩雷避坑詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
從源碼解析Python的Flask框架中request對(duì)象的用法
Flask中的request對(duì)象發(fā)送請(qǐng)求使用起來十分方便,但也有一些需要注意的地方,這里我們來從源碼解析Python的Flask框架中request對(duì)象的用法,需要的朋友可以參考下.2016-06-06
python實(shí)現(xiàn)從本地?cái)z像頭和網(wǎng)絡(luò)攝像頭截取圖片功能
這篇文章主要介紹了python實(shí)現(xiàn)從本地?cái)z像頭和網(wǎng)絡(luò)攝像頭截取圖片功能 ,文中給大家提到了python , opencv 打開網(wǎng)絡(luò)攝像頭讀取圖像的實(shí)現(xiàn)代碼,需要的朋友可以參考下2019-07-07
python+pytest自動(dòng)化測(cè)試函數(shù)測(cè)試類測(cè)試方法的封裝
這篇文章主要介紹了python+pytest自動(dòng)化測(cè)試函數(shù)測(cè)試類測(cè)試方法的封裝,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-06-06
Python3.7實(shí)現(xiàn)中控考勤機(jī)自動(dòng)連接
這篇文章主要為大家詳細(xì)介紹了Python3.7實(shí)現(xiàn)中控考勤機(jī)自動(dòng)連接,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08

