前言
考量到 decorator 的概念不好懂,def def 的語法對於從 compiling language 或者是不懂 functional programming 的人可能會覺得突兀或陌生,進而產生誤解,因此簡單打了這篇。如有錯誤敬請各位勇敢指正(但也請確定並事先驗證自己的說法),感謝大家~
一開始學 decorator 時,常常會看到類似下面的這種例子
def a_function_wrapper(inner_function):
def wrapper(*args, **kwargs):
print("The function starts!")
returned_values = \
inner_function(*args, **kwargs)
print("The function ends!")
return returned_values
return wrapper
@a_function_wrapper
def original_function():
print("I run normally~")
if __name__ == "__main__":
original_function()
第一次看到這種連續兩個 def 又 args 星號來星號去的,對於初學者可能不免感到害怕。(事實上我自己也是學了 Python 後過將近一年才懂得這個概念的,還是因為同時學了其他程式語言有類似概念才觸類旁通理解的 :cry:)
為了讓大家好理解,先從一個 scenario 開始考慮起~
為什麼我們需要 decorator 呢?
簡單說是這樣的,考慮以下情境 :::info 為了確認程式有沒有開始或完成一個函數,需要在每個函數前後做一些事情⋯⋯(例如:查看當下時間確認執行所需的時間長短、確認在執行哪個函數因此需要查看函數名稱等) :::
在還沒學 decorator 前,我們可能需要這樣
def i_am_a_dog():
print("I am a dog.")
def adder(x, y):
return x + y
if __name__ == "__main__":
# 「好麻煩!」那幾行都是為了這情境加上去的,\
# 本來只要中間那行
print("Function starts!") # 好麻煩!
i_am_a_dog()
print("Function ends!") # 好麻煩!
print("Function starts!") # 好麻煩!
print(isadder(3, 8))
print("Function ends!") # 好麻煩!
為此我們可能需要複製貼上很多行,想想看今天如果有辦法說==在每個 function 前面與後面(不改動中間的部分)加上一些動作==,我們是不是就可以達到以上的目的了呢?
先拿第一個 i_am_a_dog
開刀,原本縮排的內容是
print("I am a dog.")
現在變成
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
print("I am a dog.")
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
因此這個手術過程,可以改寫成
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
i_am_a_dog()
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
對,這跟前面做的一樣,但我們就可以寫成更 general 的版本
function_to_be_changed = i_am_a_dog
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
function_to_be_changed()
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
神奇的就在這裡:function 本身是可以當成一個變數看待的
functions as variables
這個概念各位可以想成
def func(a, b, c):
# do something
pass
的過程,其實就只是
func = ...
的一種特例罷了,如果有懂 lambda 的朋友,可以更直接的想成就是
func = lambda a, b, c: pass
這樣,但是 lambda 只限制一行(不知道各位看到這邊會不會更懂 lambda 在幹嘛了?)
傳遞參數的本質
另一件事,也希望向各位澄清:function 的 pass in 跟 return 其實就只是兩行「=」(assignment),這在幾乎所有程式語言都適用! 也就是說
def adder(x, y):
return x + y
c = adder(3, 4) # 注意我!我等等會被解體 Q_Q
就是
x, y = 3, 4 # 我就是函數括號裡的東西啦!
# 接著函數到 return 以前的東東,但 adder 沒有
c = x + y # 原本函數的位子就換成 return 後面接的東西囉~
那為什麼要這樣包起來又拆開呢?==程式設計有個三次原則:一個東西要重複三次以上,就包成函數吧!== 函數又叫「副程式」,本身可以視為一連串程式碼的包裝,方便我們把類似的過程包起來用簡單的指令表達。試想如果 adder 裡面有 300 行,然後我們要執行 10 次,而這 10 次根本只差在 x 跟 y 不同,那包成 function 豈不是方便許多?
___
好了!到此各位應該知道要怎麼做了吧~ 我們只要「再定義一個 function,把需要被處理的 function 看成一般變數(和其他 x, y 那些沒兩樣),傳進 function 對其開刀,再放回原位」就好了!
function_to_be_changed = i_am_a_dog # --> 這邊變成 \
# 新函數的 input
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
function_to_be_changed()
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
i_am_a_dog = function_to_be_changed # --> 這邊是 \
# 放回原位的動作
包成 function 囉~
def function_wrapper(function_to_be_changed):
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
function_to_be_changed()
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
return function_to_be_changed # --> 很重要!這邊 \
# 是放回原位的動作!
i_am_a_dog = function_wrapper(i_am_a_dog)
本來事情到這邊就結束了,但是當我們想對第二個函數做一樣的事情時,怪事就發生了!
function_to_be_changed = adder # --> 這邊變成 \
# 新函數的 input
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
function_to_be_changed()
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
adder = function_to_be_changed # --> 這邊是 \
# 放回原位的動作
奇怪?本來的 x 跟 y 該放到哪裡去呀? 這邊要再介紹一個技巧: *args 和 **kwargs
*args 用法
如果今天函數需要傳入的參數數量是 不固定的,這時各位也許看過但從不理解的「*」就可以來當救星了!其主要的語法意義為「打包」(即 JavaScript 中的「…」) 各位可以這樣看
x = 3, 4, 5
這樣 x 會變成一個 tuple「(3, 4, 5)」,同樣的事情如果要在 function 實現,第一招可以使用 list 或 tuple 達成
def variable_input(x):
for i in x:
print(i)
variable_input([3, 4, 5]) # 用 list
variable_input((3, 4, 5)) # 用 tuple
但這樣寫似乎沒有真正達成「參數數量可變」的目的(本質上還是一個參數),因此使用「*」可以改寫成
# 自動幫你把所有 inputs \
def variable_input(*x): # 打包成 tuple `x`
for i in x:
print(i)
variable_input(3, 4, 5)
(**kwargs 是對 func(x=3, y=4) 這種情況設計的,容我以後有機會再討論,但原理其實相似)
___
所以說,為了應付這種狀況,我們需要改成這樣
args = x, y # 這些是 \
# function_to_be_changed \
# 的參數們
function_to_be_changed = adder # --> 這邊變成 \
# 新函數的 input
# ----------- 以下是新函數的內部 ----------- #
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
output = function_to_be_changed(*args)
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
# ----------- 以上是新函數的內部 ----------- #
wrapper_output = output # 內部的 output, \
# 不知道塞哪裡欸?
adder = function_to_be_changed # --> 這邊是 \
# 放回原位的動作
為了應付 args 和 wrapper_output 不知道該放哪裡,如果今天這個
adder = new_function(adder)
可以更彈性就好了⋯⋯既然中間都要做一堆事,那定義成 function 是不是就解決了?像這樣
args = x, y # 這些是 \
# function_to_be_changed \
# 的參數們
function_to_be_changed = adder # --> 這邊變成 \
# 新函數的 input
# ----------- 以下是新函數的內部 ----------- #
def some_process(*args):
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
output = function_to_be_changed(*args)
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
return output
# ----------- 以上是新函數的內部 ----------- #
adder_new = some_process # --> 這邊是 \
# 放回原位的動作
# 要把東西塞進 adder_new 就直接
wanted_output = adder_new(*args)
# 這樣因為 adder_new 就是 some_process
# 所以便會啟動所有包裝過的動作,
# 然後把 output 放在 wanted_output,達到目標
娃~看起來好可怕呀~再直接把新函數寫出來就變這樣囉~
args = x, y
def new_function(function_to_be_changed):
# ----------- 以下是新函數的內部 ----------- #
def some_process(*args):
print("Function starts!") # 好麻煩!
# vvvvvv 注意下面都不會動! vvvvvv
output = function_to_be_changed(*args)
# ^^^^^^ 注意上面都不會動! ^^^^^^
print("Function ends!") # 好麻煩!
return output
# ----------- 以上是新函數的內部 ----------- #
return some_process # --> 這邊是 \
# 放回原位的動作
adder_new = new_function(adder)
# 要把東西塞進 adder_new 就直接
wanted_output = adder_new(*args)
# 這樣因為 adder_new 就是 some_process
# 所以便會啟動所有包裝過的動作,
# 然後把 output 放在 wanted_output,達到目標
這就是兩個 def 的由來了,「為了讓 function 可以回傳 function」 那由於舊的 adder 不再被需要了,最後兩行可以直接改成
adder = new_function(adder) # <-- 注意!我之前出現過!
# 要使用時:
# >>> 方法一
wanted_output = adder(*args) # 這跟沒經過包裝沒兩樣
# >>> 方法二
wanted_output = adder(x, y) # 不用多個 args 也可以~
於是觀察這兩行
i_am_a_dog = new_function(i_am_a_dog)
adder = new_function(adder)
同樣的語法有些累贅,Python 這時提供了一個 語法糖 就是 decorator,我們只要把原本的函數定義加上 @xxxx
就可以少掉這行!
@new_function
def i_am_a_dog():
# ...
pass
這就是 decorator 的由來囉~ 簡單把握兩個原則—— :::info decorator 是一個函數,輸入是函數,輸出也是函數 :::
然後 :::danger 為了讓輸入的(內)函數可以保有輸入輸出值的成為(外)輸出回傳,因此會有第二層 def 來代表那個被輸入的(內)函數,然後對其進行處理後輸出。 :::
完蛋了上面像繞口令⋯⋯簡單說
def 被包裝函數(): pass def 裝飾器(某函數): # something def 替身函數(): # <-- 作為`被包裝函數`的替身 某函數() # something return 替身函數
有興趣了解更深入的可以去探索「closure(閉包)」這個概念,但其實只要把握上面兩個原則應該就行了。 (JavaScript 用到超多的想到快死了 T_T)