Decorator 概念簡介

python

前言

考量到 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)

發表迴響