在 Python 裝飾器的學習中,“被裝飾函數的參數如何傳遞到裝飾器內層函數”是一個高頻疑問點。很多開發者能寫出裝飾器的基本結構,卻對參數傳遞的底層邏輯一知半解。本文將以一段具體代碼為例,把參數傳遞過程拆成“裝飾器生效→調用觸發→參數捕獲→參數轉交”4個階段,逐行解析 Python 解釋器的執行邏輯,帶你看清每一個細節。
一、先看完整代碼:我們的分析樣本
為了讓分析更聚焦,先給出完整的裝飾器與被裝飾函數代碼,后續所有拆解都圍繞這段代碼展開:
# 1. 定義裝飾器函數 zhuangshiqi
def zhuangshiqi(func):print("zhuangshiqi里") # 裝飾器生效時執行# 2. 定義裝飾器內層函數 hanshu(參數傳遞的核心載體)def hanshu(*arg, **kwarg):print("hanshu里") # 被裝飾函數調用時執行print(f"捕獲的位置參數:{arg}") # 打印捕獲的位置參數print(f"捕獲的關鍵字參數:{kwarg}") # 打印捕獲的關鍵字參數return func(*arg, **kwarg) # 將參數轉交原函數return hanshu # 裝飾器返回內層函數# 3. 用 @ 語法糖為函數 ts 綁定裝飾器
@zhuangshiqi
def ts(a, b, /, c, d, *e, f, args, **kwargs):"""被裝飾的目標函數,包含多種參數類型"""return "函數執行結束"# 4. 調用被裝飾后的 ts 函數
ts(1, 2, 3, 4, 1, f=2, args=3, kwargs=4)
這段代碼包含了 Python 中常見的參數類型(強制位置參數 /
、可變位置參數 *e
、關鍵字-only 參數 f
、可變關鍵字參數 **kwargs
),以及標準的裝飾器嵌套結構,是分析參數傳遞的理想樣本。
二、階段1:裝飾器生效(@zhuangshiqi
的“魔法”)——ts
的指向被偷偷替換
很多人誤以為 @zhuangshiqi
只是一個“標記”,告訴 Python“這個函數需要被裝飾”。但實際上,@
是 Python 的語法糖,它會在解釋器讀取到函數定義時立即執行,完成“目標函數指向替換”的核心操作。這一步是參數能傳遞到內層函數的前提,必須先徹底理解。
1.2 解釋器的執行順序(逐行拆解)
當 Python 解釋器運行這段代碼時,會按以下順序處理:
步驟1:讀取裝飾器定義,但不執行
解釋器先讀取 def zhuangshiqi(func):...
,在內存中創建一個名為 zhuangshiqi
的函數對象,此時僅完成“定義”,不會執行函數內部的代碼(比如 print("zhuangshiqi里")
不會觸發)。
步驟2:讀取 @zhuangshiqi
,記錄裝飾器綁定關系
解釋器讀取到 @zhuangshiqi
時,會記住:“接下來定義的函數 ts
,需要用 zhuangshiqi
這個裝飾器處理”。這里的 @
語法,本質是后續操作的“觸發器”。
步驟3:定義原函數 ts
,此時 ts
指向原函數
解釋器繼續讀取 def ts(a, b, /, c, d, *e, f, args, **kwargs):...
,在內存中創建一個“原 ts
函數對象”,此時變量 ts
指向這個原函數對象(可以理解為:ts
是原函數的“名字標簽”)。
步驟4:觸發裝飾器邏輯,完成 ts
指向替換(關鍵!)
這是最核心的一步:@zhuangshiqi
等價于執行 ts = zhuangshiqi(ts)
。解釋器會按以下子步驟執行:
- 子步驟4.1:將“原
ts
函數對象”作為參數func
,傳入裝飾器zhuangshiqi
; - 子步驟4.2:執行
zhuangshiqi(func)
內部代碼:- 首先執行
print("zhuangshiqi里")
,所以運行代碼時,你會先看到這句話(不是調用ts
時才看到); - 然后定義內層函數
hanshu(*arg, **kwarg)
:在內存中創建hanshu
函數對象,此時hanshu
僅完成定義,不執行內部代碼; - 最后執行
return hanshu
:將zhuangshiqi
的返回值(即hanshu
函數對象)賦值給變量ts
;
- 首先執行
- 子步驟4.3:最終結果:變量
ts
不再指向“原ts
函數對象”,而是指向“內層函數hanshu
對象”。
1.3 關鍵結論:ts
成了 hanshu
的“別名”
經過階段1后,當你后續寫 ts(...)
時,Python 解釋器會認為你要調用的是 hanshu(...)
——這是參數能傳遞到 hanshu
的根本原因。如果把函數比作“人”,ts
這個“名字”原本屬于原函數,現在被裝飾器“偷換”給了 hanshu
。
三、階段2:調用 ts(...)
——實際是調用 hanshu(...)
,參數傳遞的起點
當你執行 ts(1, 2, 3, 4, 1, f=2, args=3, kwargs=4)
時,因為階段1已經完成了“名字替換”,Python 會直接調用 hanshu(1, 2, 3, 4, 1, f=2, args=3, kwargs=4)
。這一步是參數傳遞的“起點”,所有傳給 ts
的參數,都會原封不動地傳給 hanshu
。
2.1 先明確:傳給 ts
的參數類型
在分析傳遞過程前,先拆解你傳入的參數 1, 2, 3, 4, 1, f=2, args=3, kwargs=4
,按 Python 的參數分類規則,它們可分為兩類:
- 位置參數:
1, 2, 3, 4, 1
——沒有key=
前綴,靠“傳入順序”匹配函數形參; - 關鍵字參數:
f=2, args=3, kwargs=4
——有key=
前綴,靠“key
與形參名匹配”來綁定。
這兩類參數會被 Python 打包成一個“參數集合”,完整傳遞給 hanshu
。
四、階段3:hanshu(*arg, **kwarg)
——“全捕獲”所有參數,打包成元組和字典
hanshu
的形參定義 (*arg, **kwarg)
是 Python 中“捕獲任意參數”的標準寫法,它能把傳給 hanshu
的所有參數,按“位置/關鍵字”分類打包,這是參數傳遞的核心環節。
4.1 *arg
:捕獲所有位置參數,打包成元組
- 作用:
*arg
中的*
是“位置參數捕獲符”,它會把傳給hanshu
的所有“位置參數”(即1, 2, 3, 4, 1
)按傳入順序收集起來,打包成一個元組(tuple),并賦值給變量arg
; - 執行結果:
arg = (1, 2, 3, 4, 1)
; - 為什么用元組:元組是“不可變序列”,一旦創建就不能修改。這樣設計能避免參數在
hanshu
中被意外篡改,確保后續轉交給原函數的參數是“原始值”。
4.2 **kwarg
:捕獲所有關鍵字參數,打包成字典
- 作用:
**kwarg
中的**
是“關鍵字參數捕獲符”,它會把傳給hanshu
的所有“關鍵字參數”(即f=2, args=3, kwargs=4
)按“key:value
”的形式收集起來,打包成一個字典(dict),并賦值給變量kwarg
; - 執行結果:
kwarg = {'f': 2, 'args': 3, 'kwargs': 4}
; - 為什么用字典:關鍵字參數的核心是“通過名字匹配形參”,字典的
key
正好能對應參數名,value
對應參數值,結構清晰且便于后續“解包”。
4.3 驗證捕獲結果:打印參數
此時 hanshu
內部的 print(f"捕獲的位置參數:{arg}")
和 print(f"捕獲的關鍵字參數:{kwarg}")
會執行,輸出結果如下:
捕獲的位置參數:(1, 2, 3, 4, 1)
捕獲的關鍵字參數:{'f': 2, 'args': 3, 'kwargs': 4}
這證明 hanshu
已經完整捕獲了所有傳給 ts
的參數,沒有遺漏。
五、階段4:return func(*arg, **kwarg)
——參數“解包”,轉交原 ts
函數
hanshu
捕獲參數后,不是自己使用,而是要“轉交”給“原 ts
函數”(也就是階段1中傳入 zhuangshiqi
的 func
,因為 func
指向原 ts
)。這里的 *arg
和 **kwarg
又承擔了新的角色——“參數解包器”。
5.1 *arg
:把元組“解包”成原始位置參數
arg
是 (1, 2, 3, 4, 1)
,在 func(*arg)
中,*
會把元組“拆成單個的位置參數”,即:
*arg
→ 拆成 1, 2, 3, 4, 1
(和你最初傳給 ts
的位置參數完全一樣)。
5.2 **kwarg
:把字典“解包”成原始關鍵字參數
kwarg
是 {'f': 2, 'args': 3, 'kwargs': 4}
,在 func(**kwarg)
中,**
會把字典“拆成 key=value
形式的關鍵字參數”,即:
**kwarg
→ 拆成 f=2, args=3, kwargs=4
(和你最初傳給 ts
的關鍵字參數完全一樣)。
5.3 最終轉交:原 ts
拿到完整參數,正常執行
func(*arg, **kwarg)
等價于直接調用原 ts
函數:
原ts(1, 2, 3, 4, 1, f=2, args=3, kwargs=4)
——這正是你最初想調用的參數組合!
此時原 ts
會按自己的形參定義(a, b, /, c, d, *e, f, args, **kwargs
)匹配參數:
a=1, b=2
:/
左側的強制位置參數,只能通過位置匹配;c=3, d=4
:/
右側的普通位置參數,通過位置匹配;*e=(1)
:可變位置參數,收集剩下的位置參數1
;f=2
:關鍵字-only 參數(*e
之后的參數默認是關鍵字-only),通過f=2
匹配;args=3
:普通關鍵字參數,通過args=3
匹配;**kwargs={'kwargs':4}
:可變關鍵字參數,收集剩下的關鍵字參數kwargs=4
。
原 ts
接收參數后,執行 return "函數執行結束"
,這個返回值會被 hanshu
接收,再返回給 ts()
的調用者——整個參數傳遞鏈路完全閉環。
六、總結:參數傳遞的“完整鏈路圖”與形象類比
6.1 鏈路圖:用一句話串起所有階段
你調用 ts(參數) → 因 @zhuangshiqi 替換,實際調用 hanshu(參數) → hanshu 用 *arg/**kwarg 把參數分別打包成元組/字典 → hanshu 用 */* 把元組/字典拆成原始參數 → 轉交原 ts 執行 → 原 ts 返回結果,hanshu 再把結果返回給你
6.2 形象類比:用“快遞運輸”理解參數傳遞
如果把參數比作“快遞”,整個過程就像一次完整的快遞運輸:
- 你(調用者)把“快遞”(參數)交給“快遞點 ts”;
- 但“快遞點 ts”的招牌被“總公司 zhuangshiqi”換成了“中轉站 hanshu”(裝飾器生效階段);
- “中轉站 hanshu”把“快遞”按“類型”(位置/關鍵字)分類打包:位置參數裝成“紙箱”(元組),關鍵字參數裝成“快遞袋”(字典)(參數捕獲階段);
- “中轉站 hanshu”把打包好的“快遞”拆回原樣,交給“最終收件人原 ts”(參數轉交階段);
- “原 ts”處理完快遞(執行函數),把“回執”(返回值)通過“中轉站 hanshu”交回給你。
七、關鍵注意點:避免參數傳遞中的常見問題
*arg, **kwarg
的順序不能亂:hanshu
的形參必須是(*arg, **kwarg)
,不能寫成(**kwarg, *arg)
——Python 規定,位置參數捕獲符*arg
必須在關鍵字參數捕獲符**kwarg
之前,否則會報語法錯誤;- 裝飾器生效時機:
@zhuangshiqi
會在函數定義時立即執行,不是調用時執行——如果裝飾器內有耗時操作(如讀取文件),會在程序啟動時就觸發,可能影響啟動速度; - 原函數元信息保留:本文的示例中,
hanshu
沒有保留原ts
的元信息(如__name__
、__doc__
),調用print(ts.__name__)
會輸出hanshu
,而非ts
。實際開發中,建議用functools.wraps
保留元信息,修改后的hanshu
如下:
import functools
def zhuangshiqi(func):@functools.wraps(func) # 保留原函數元信息def hanshu(*arg, **kwarg):# 原有邏輯return func(*arg, **kwarg)return hanshu
通過以上4個階段的拆解,相信你已經徹底理解了裝飾器中參數傳遞的每一個細節。Python 裝飾器的參數傳遞,本質是“名字替換+參數打包+參數解包”的組合,只要抓住這三個核心操作,無論遇到多復雜的裝飾器結構,都能理清參數的流向。