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