概要
- 什麼是裝飾器
- 怎麼使用裝飾器
- 使用場景
- 如何給裝飾器傳遞引數
1. 什麼是裝飾器
能夠對其他函式的功能進行增強。也就是函式程式碼功能重用。它是一個設計模式。需要注意的是:
- 裝飾器本身是一個函式
- 增強被裝飾函式的功能,同時被增強的函式還不能感受到自己被增強了或者說被修了
- 裝飾器需要接受一個函式物件作為引數以對其進行包裝
- 不會改變被呼叫函式的呼叫方式
2. 怎麼使用裝飾器
要理解裝飾器你需要理解如下內容:
- 函式即變數 def 定義一個函式就等於把函式體賦值給了一個變數(函式名)
- 高階函式 滿足兩個條件 把一個函式名當做形參傳入另外一個函式、返回值中包含函式名
- 巢狀函式 函式中巢狀函式
裝飾器=高階函式+函式巢狀
2.1 函式巢狀
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com def fun3(): print("in the fun3") def fun4(): print("in the fun4") fun4() if __name__ == '__main__': fun3()
執行結果
在fun3中定義一個fun4函式,執行fun3,同時也會執行fun4。這個比較好理解就是函式中巢狀一個函式,對於巢狀的函式(這裡是fun4)你可以在你呼叫的函式(這裡是fun3)中執行,也可以把這個fun4返回。
2.2 高階函式
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com def fun1(): print("Hello fun1") def fun2(fun): print("World fun2") # 這裡是執行的傳入的函式本身 fun() # 返回回去 return fun # 執行fun2同時把fun1當做引數傳遞進去 fun2(fun1)
上面滿足高階函式的2個條件。但是如果我只是想給fun1增加功能,那麼我這樣執行顯然成功了,可是我改變了呼叫方式,也就是說我必須執行fun2(fun1)才行,而我程式中很多地方寫的都是fun1(),那如果我這樣改起來自己麻煩同時如果別人調我的fun1(),那別人也要改。如何不改變呼叫方式呢?透過下面的方式呼叫,不過這不再是單純的給fun1增加功能,因為最後相當於重新定義了fun1。
# 如何不改變呼叫方式也增加新功能呢?就這樣。這裡相當於把之前的fun1重新定義了 fun1 = fun2(fun1)
2.3 裝飾器如何使用
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com import time def timer(func): def wapper(*args, **kwargs): start_time = time.time() func() stop_time = time.time() print("耗時:", stop_time - start_time) return wapper @timer def run(): print("To do job.") time.sleep(2) print("Job is done.") run()
我這裡是給run函式增加一個功能計算該函式的執行時間。上面就是裝飾器的用法。增加了功能、沒有改變原來的呼叫方式,timer函式隨處可以引用。如果上面的功能透過高階函式如何實現呢?你看下面的程式碼幾乎完全相同,只是run()函式上面去掉了@timer,以及後面的執行語句前面有加了一個語句,正像上面所說執行完 run = timer(run) 其實就等於重新定了run()。
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com import time def timer(func): def wapper(*args, **kwargs): start_time = time.time() func() stop_time = time.time() print("耗時:", stop_time - start_time) return wapper def run(): print("To do job.") time.sleep(2) print("Job is done.") run = timer(run) run()
加上 @timer 之後,執行 run()就等於執行 timer函式中的wapper。因為:如果不加 @timer 你可以這麼用,其實這麼用的效果和加 @timer 然後呼叫run()是一樣的。
加上 @timer 之後,執行過程是什麼樣的呢?
- 從上到下執行,遇到 def timer(func) 函式直譯器知道是定義了一個函式,因為此時沒有呼叫所以直譯器繼續向下執行
- 遇到@timer,直譯器執行 def timer(func) 並把 run() 函式傳遞給 def timer(func) 中的 func,然後發現定義了一個函式 def wapper,它此時也不執行,繼續向下,遇到return wapper 則返回wapper函式
- 此時裝飾完畢其實也就是對run()函式進行了重新賦值,繼續向下,遇到run()這條語句,也就是我們直接呼叫,這時候它會去執行之前返回的wapper函式,此時run()就是wapper()函式 理解為 run = timer(run)
- 執行wapper函式的第一條語句 start_time = time.time()
- 執行wapper函式的第二條語句 func() 而這時候的func()就是之前傳遞進來的run()函式,此時執行run()函式的語句,直到執行完成返回
- 執行執行wapper函式第三條語句 stop_time = time.time()
- 執行執行wapper函式第四條語句 print("耗時:", stop_time - start_time)
2.4 裝飾器如何傳遞引數
我們可能注意到 def wapper(*args, **kwargs) 其實這裡就可以傳遞引數。
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: rex.cheny # E-mail: rex.cheny@outlook.com def printName(func): def wapper(*args, **kwargs): func(**kwargs) return wapper @printName def run(name): print("姓名為:", name) def main(): run(name="chen") if __name__ == '__main__': main()
可能有些人對 **kwargs 有些迷惑,其實它就是一個字典形式,你傳進來的引數必須是 KEY=VALUE形式,就行我在main()函式中的那樣 run(name="chen"),如果還不明白我換一種寫法如下圖:
2.5 類中如何使用裝飾器
#!/usr/bin/env python # -*- coding: utf-8 -*- """ """ import sys import time def timer(tagName): import time def wapper(func): def aa(self, *args, **kwargs): start = time.time() func(self) end = time.time() consume = end - start if consume > 60: min, sec = divmod(consume, 60) print(" - %s 執行耗時:%s 分 %s 秒。" % (tagName, str(min), str(sec))) else: print(" - %s 執行耗時:%s 秒。" % (tagName, str(consume))) return aa return wapper class AA: def __init__(self): pass @timer(tagName="runTask方法") def runTask(self): time.sleep(2) if __name__ == "__main__": try: aa = AA() aa.runTask() except Exception as err: print(err) finally: sys.exit()
3. 例項
我這裡模擬一個場景就是網站登入,我們需要給所有頁面增加一個驗證功能。
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com """ 模擬網站登入,也就是給某些頁面加驗證功能 """ users = {"Tom": "123"} # 驗證功能 def auth(func): def wrapper(*args, **kwargs): username = input("Input your username: ") password = input("Input your password: ") if username.strip() in users and password.strip() == users[username]: func(username=username) # 如果你需要接收被裝飾函式的返回值就要 # res = func(username=username) else: exit("Invalid username or password.") return wrapper # 下面是模擬兩個頁面 def indexPage(): print("Welcome to Index.") # 給home頁面加驗證 @auth def homePage(**kwargs): print("Welcome: ", kwargs["username"], "to Home.") return "You are Home." def main(): indexPage() homePage() if __name__ == '__main__': main()
執行結果
現在的需求變了,我又增加了一個頁面,同時這個頁面需要另外一種驗證方式,怎麼辦?最笨的方法是再寫一個裝飾器用於其他驗證方式,有沒有更好的辦法呢?就是設定裝飾器引數。
#!/usr/bin/env python # Author: rex.cheny # E-mail: rex.cheny@outlook.com """ 模擬網站登入,也就是給某些頁面加驗證功能 """ users = {"Tom": "123"} def authG_two(auth_type="local"): """ 這個裝飾器帶了引數,所以它就不能寫fun,而必須是明確的引數。而且這裡和之前比又多了一層, 因為最外層不是傳遞的函式進來,而是引數。這個是如何執行的呢? @authG_two(auth_type="local") def fun1(): pass fun1 = authG_two(auth_type="local") 然後返回 wrapper,這時候 fun1 = wrapper(fun1),然後在進入到 authG_two() 執行 wrapper()方法 根據auth_type返回 對應的函式,如果是local 最終則是 fun1 = localAuth()並且 localAuth()裡的func函式就是之前傳遞進去的func函式。 """ def wrapper(func): # 本地驗證方式 def localAuth(*args, **kwargs): username = input("Input your username: ") password = input("Input your password: ") if username.strip() in users and password.strip() == users[username]: print("Local authentication succeed.") func(username=username) # 如果你需要接收被裝飾函式的返回值就要 # res = func(username=username) else: exit("Invalid username or password.") # LDAP驗證方式 def ldapAuth(*args, **kwargs): username = input("Input your username: ") password = input("Input your password: ") func(username=username) print("LDAP authentication succeed.") if auth_type == "local": return localAuth else: return ldapAuth return wrapper """ 下面三個模仿三個板塊的主頁 """ def indexPage(): print("Welcome to Index.") @authG_two() def homePage(**kwargs): """ 這裡使用新的裝飾器,它後面帶括號,不設定參數列示使用預設引數設定。 """ print("Welcome: ", kwargs["username"], "to Home.") return "You are Home." # 新增BBS頁面 @authG_two(auth_type="ldap") def bbsPage(**kwargs): """ 這裡使用新的裝飾器,它後面帶括號,這就說明那個裝飾器需要帶引數,驗證方式為LDAP """ print("Welcome to BBS.") def main(): indexPage() homePage() bbsPage() if __name__ == '__main__': main()
執行結果