Python裝飾器decorator實際應(yīng)用與高級使用詳解
在這篇文章中,我們來詳細(xì)、系統(tǒng)地講解一下Python的裝飾器(Decorator)。我會從基本概念講起,循序漸進(jìn),直到實際應(yīng)用和高級用法。
1. 什么是裝飾器?(核心思想)
裝飾器本質(zhì)上是一個Python函數(shù),它可以讓其他函數(shù)在不改變其源代碼的情況下增加額外的功能。
核心思想:包裝 (Wrapping)。
你可以把裝飾器想象成一個包裝禮物的過程:
- 你的函數(shù) = 禮物本身
- 裝飾器 = 包裝紙、彩帶和蝴蝶結(jié)
禮物(函數(shù))的核心功能沒有變,但經(jīng)過包裝(裝飾器)后,它看起來更漂亮、更完整了(增加了新功能)。
在代碼層面,裝飾器是一個接收函數(shù)作為參數(shù),并返回一個新函數(shù)的函數(shù)。
2. 為什么需要裝飾器?(動機)
假設(shè)我們有很多函數(shù),現(xiàn)在想給每個函數(shù)都加上一個“計算運行時間”的功能。
不好的方法:修改每個函數(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耗時: {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耗時: {end_time - start_time:.2f}秒") # ... 還有 func_C, func_D ...
問題:
- 代碼冗余:計時邏輯在每個函數(shù)里都重復(fù)了一遍。
- 違反“開放封閉原則”:每次新增需求(比如再加個日志功能),都要去修改已有函數(shù)的代碼。
裝飾器就是為了解決這類問題而生的,它能將這些通用的、與核心業(yè)務(wù)無關(guān)的功能(如計時、日志、認(rèn)證)抽離出來。
3. Python基礎(chǔ):理解裝飾器的前提
要理解裝飾器,必須先理解Python中 “函數(shù)是一等公民” (Functions are First-Class Objects) 的概念。這意味著函數(shù)可以:
被賦值給一個變量
def say_hello(): print("Hello!") greet = say_hello # 將函數(shù)say_hello賦值給變量greet greet() # 輸出: Hello!
作為參數(shù)傳遞給另一個函數(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() # 輸出: 你好,世界!
裝飾器正是利用了這幾點特性,尤其是第2點和第3點。
4. 一步步構(gòu)建你的第一個裝飾器
最簡單的裝飾器(手動方式)
我們來寫一個簡單的裝飾器,它在函數(shù)執(zhí)行前后打印信息。
# 1. 定義一個裝飾器函數(shù) def my_decorator(func): # 2. 定義一個內(nèi)部包裝函數(shù),它將“包裝”原始函數(shù) def wrapper(): print("在被裝飾的函數(shù)執(zhí)行之前...") func() # 3. 調(diào)用原始函數(shù) print("在被裝飾的函數(shù)執(zhí)行之后...") # 4. 返回這個包裝好的函數(shù) return wrapper # 定義一個需要被裝飾的函數(shù) def say_whee(): print("Whee!") # 手動使用裝飾器 say_whee = my_decorator(say_whee) # 現(xiàn)在調(diào)用say_whee,實際上是在調(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提供了一個更優(yōu)雅、更Pythonic的方式來使用裝飾器,那就是 @
語法糖。
def my_decorator(func): def wrapper(): print("在被裝飾的函數(shù)執(zhí)行之前...") func() print("在被裝飾的函數(shù)執(zhí)行之后...") return wrapper @my_decorator # 這行等價于 say_whee = my_decorator(say_whee) def say_whee(): print("Whee!") say_whee()
輸出和上面完全一樣,但代碼是不是簡潔多了?
5. 處理帶參數(shù)的函數(shù)
如果我們的原始函數(shù)帶參數(shù)怎么辦?比如 greet(name)
。上面的 wrapper
函數(shù)不接受任何參數(shù),會報錯。
我們需要讓 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的重要性
使用裝飾器有一個小問題:它會丟失原始函數(shù)的一些元信息(metadata),比如函數(shù)名 (__name__
)、文檔字符串 (__doc__
)等。
@decorator_with_args def greet(name): """這是一個打招呼的函數(shù)""" print(f"你好, {name}!") print(greet.__name__) # 輸出: wrapper (而不是 greet) print(greet.__doc__) # 輸出: None (而不是 "這是一個打招呼的函數(shù)")
這對于調(diào)試和自省工具來說非常不便。為了解決這個問題,Python的 functools
模塊提供了一個專門的裝飾器:@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): """這是一個打招呼的函數(shù)""" print(f"你好, {name}!") print(greet.__name__) # 輸出: greet print(greet.__doc__) # 輸出: 這是一個打招呼的函數(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
),并返回一個真正的裝飾器。 - 中間層函數(shù) (
decorator_repeat
):這就是我們之前寫的標(biāo)準(zhǔn)裝飾器,它接收一個函數(shù)作為參數(shù)。 - 最內(nèi)層函數(shù) (
wrapper
):執(zhí)行增強后的邏輯。
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
,這等價于say_hello = decorator_repeat(say_hello)
。 - 最終
say_hello
變量指向了wrapper
函數(shù)。
8. 類裝飾器(進(jìn)階)
除了用函數(shù),我們也可以用類來創(chuàng)建裝飾器。這在需要維護(hù)狀態(tài)時特別有用。一個類要成為裝飾器,需要實現(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
類的實例 some_function
能夠記住自己被調(diào)用的次數(shù)。
9. 裝飾器的實際應(yīng)用場景(重點)
日志記錄 (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)
性能計時 (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)中非常常見。
# 這是一個簡化的概念性例子 import functools def login_required(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 偽代碼:假設(shè)有一個函數(shù) check_user_logged_in() if check_user_logged_in(): return func(*args, **kwargs) else: raise PermissionError("用戶未登錄,禁止訪問!") return wrapper # 模擬一個全局登錄狀態(tài) _user_is_logged_in = False def check_user_logged_in(): return _user_is_logged_in @login_required def view_profile(): print("這是用戶的個人資料頁面。") # 嘗試調(diào)用 try: view_profile() except PermissionError as e: print(e) # 輸出: 用戶未登錄,禁止訪問! # 模擬用戶登錄 _user_is_logged_in = True print("\n用戶登錄后:") view_profile() # 輸出: 這是用戶的個人資料頁面。
緩存 (Caching/Memoization)
對于計算成本高的函數(shù),可以將結(jié)果緩存起來。Python 3.9+ 自帶了 functools.cache
。我們也可以自己實現(xià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自帶的會更高效 # 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"第一次計算耗時: {end - start:.4f}s") start = time.time() print(fibonacci(35)) end = time.time() print(f"第二次(從緩存)計算耗時: {end - start:.4f}s")
輸入驗證 (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") # 會拋出 TypeError except TypeError as e: print(e)
10. 裝飾器棧(多個裝飾器)
一個函數(shù)可以被多個裝飾器修飾。執(zhí)行順序是從下到上(最靠近函數(shù)的先被應(yīng)用),然后執(zhí)行時是從上到下。
@timer @log_function_call def complex_calculation(x, y): time.sleep(0.5) return x * y + 2
這等價于:
complex_calculation = timer(log_function_call(complex_calculation))
執(zhí)行 complex_calculation(3, 4)
時:
timer
的wrapper
開始執(zhí)行,記錄開始時間。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é)束時間,并打印耗時。
11. 總結(jié)
- 核心:裝飾器是一個接收函數(shù)并返回新函數(shù)的函數(shù),用于在不修改原函數(shù)代碼的情況下增加功能。
- 基礎(chǔ):依賴于Python中函數(shù)是“一等公民”的特性。
- 語法糖:
@decorator
是my_func = decorator(my_func)
的簡寫。 - 通用性:使用
*args, **kwargs
來處理任意參數(shù)的函數(shù)。 - 最佳實踐:始終使用
@functools.wraps
來保留原函數(shù)的元信息。 - 靈活性:裝飾器可以帶參數(shù),也可以用類來實現(xiàn)。
- 強大應(yīng)用:日志、計時、認(rèn)證、緩存、驗證等都是裝飾器的經(jīng)典用例,極大地提高了代碼的復(fù)用性和可維護(hù)性。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Python網(wǎng)絡(luò)編程中urllib2模塊的用法總結(jié)
使用urllib2模塊進(jìn)行基于url的HTTP請求等操作大家也許都比較熟悉,這里我們再深入來了解一下urllib2針對HTTP的異常處理相關(guān)功能,一起來看一下Python網(wǎng)絡(luò)編程中urllib2模塊的用法總結(jié):2016-07-07Python中read,readline和readlines的區(qū)別案例詳解
這篇文章主要介紹了Python中read,readline和readlines的區(qū)別案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09Python常見字符串操作函數(shù)小結(jié)【split()、join()、strip()】
這篇文章主要介紹了Python常見字符串操作函數(shù),結(jié)合實例形式總結(jié)分析了split()、join()及strip()的常見使用技巧與注意事項,需要的朋友可以參考下2018-02-02