文章目錄
一、裝飾器簡介
1. 裝飾器是什么?
概括地講,裝飾器的作用就是在不修改被裝飾對象源代碼和調用方式的前提下為被裝飾對象添加額外的功能。
裝飾器經常用於比如:插入日志、性能測試、事務處理、緩存、權限校驗等場景。
有了裝飾器,就可以抽離出大量與函數功能本身無關的雷同代碼並繼續重用。
2. 為什么用裝飾器?
軟件的設計應該遵循開放封閉原則,即對擴展是開放的,而對修改是封閉的。
在軟件設計完成后,不想改部分源碼又想添加新功能,就用到了裝飾器。

二、裝飾器的實現
函數裝飾器分為:無參裝飾器和有參裝飾兩種,二者的實現原理都是“函數嵌套+閉包+函數對象”的組合使用。
1. 閉包
閉包(Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。
這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。
def print_msg(): # print_msg是外圍函數
msg = "I'm closure"
def printer(): # printer是嵌套函數
print(msg)
return printer
closure = print_msg() # 這里獲得的就是一個閉包
closure() # 輸出 I'm closure
msg是一個局部變量,在print_msg函數執行之后就不會存在了。
但是嵌套函數引用了這個變量,將這個局部變量封閉在了嵌套函數中,這樣就形成了一個閉包。
2. 裝飾器語法糖
語法糖(Syntactic sugar),也譯為糖衣語法,指計算機語言中添加的某種語法。
這種語法對語言的功能並沒有影響,但是更方便程序員使用。
通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。
@ 符號是裝飾器的語法糖。它放在一個函數開始定義的地方(頭頂),和這個函數綁定在一起。
在我們調用這個函數的時候,會先將這個函數做為參數傳入它頭頂,即裝飾器里。
3. 時間計時器
以下用裝飾器來實現計算一個函數的執行時長,讓函數睡眠3秒。
# 這是裝飾函數
def timer(func):
def wrapper(*args, **kw):
start_time = time.time()
func(*args, **kw) # 這是函數真正執行的地方
stop_time = time.time()
cost_time = stop_time - start_time
print("花費時間:{}秒".format(cost_time))
return wrapper
import time
@timer
def want_sleep(sleep_time):
time.sleep(sleep_time)
want_sleep(3)
4. 裝飾器中@wraps作用
裝飾器(decorator)在實現的時候,被裝飾后的函數其實已經是另外一個函數了(函數名等函數屬性會發生改變)。
為了不影響,Python的functools包中提供了一個叫wraps的裝飾器來消除這樣的副作用。
寫一個裝飾器的時候,最好在實現之前加上functools中的wraps,它能保留原有函數的名稱和文檔字符串(DocStrings)。
文檔字符串用於解釋文檔程序,幫助程序文檔更加簡單易懂。
可以在函數體的第一行使用一對三個單引號 ‘’’ 或者一對三個雙引號 “”" 來定義文檔字符串。
使用 doc(注意雙下划線)調用函數中的文檔字符串屬性。
- 不使用@wraps裝飾器
def decorator(func):
"""this is decorator __doc__"""
def wrapper(*args, **kwargs):
"""this is wrapper __doc__"""
print("this is wrapper method")
return func(*args, **kwargs)
return wrapper
@decorator
def test():
"""this is test __doc__"""
print("this is test method")
print("__name__: ", test.__name__)
print("__doc__: ", test.__doc__)
運行結果:
name: wrapper
doc: this is wrapper doc
分析:
對test()方法進行裝飾時候,實際上是
test = decorator(test)
返回的是wrapper方法的引用,也就是讓test指向了wrapper方法,所以調用test.name, 實際上是wrapper.name。
這造成后面查找該方法的名字和注釋時得到裝飾器內嵌函數的名字和注釋。
- 使用@wraps裝飾器解決這個問題
from functools import wraps
def decorator(func):
"""this is decorator __doc__"""
@wraps(func)
def wrapper(*args, **kwargs):
"""this is wrapper __doc__"""
print("this is wrapper method")
return func(*args, **kwargs)
return wrapper
@decorator
def test():
"""this is test __doc__"""
print("this is test method")
print("__name__: ", test.__name__)
print("__doc__: ", test.__doc__)
運行結果:
name: test
doc: this is test doc
5. 裝飾器順序
一個函數可以同時定義多個裝飾器,比如:
@a
@b
@c
def f ():
pass
它的執行順序是從里到外,最先調用最里層的裝飾器,最后調用最外層的裝飾器,它等效於:
f = a(b(c(f)))
三、裝飾器的參數
1. 無參類裝飾器
裝飾器不僅可以是函數,還可以是類。
基於類裝飾器的實現,必須實現 call 和 __init__兩個內置函數。
當使用 @ 形式將裝飾器附加到函數上時,就會調用此方法。
init :接收被裝飾函數
call :實現裝飾邏輯。
class logger(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[INFO]: the function {func}() is running..." \
.format(func=self.func.__name__))
return self.func(*args, **kwargs)
@logger
def say(something):
print("say {}!".format(something))
say("hello")
運行結果:
[INFO]: the function say() is running…
say hello!
2. 有參類裝飾器
上面不帶參數的例子只能打印INFO級別的日志。
當需要打印DEBUG、WARNING等級別的日志時就需要給類裝飾器傳入參數,給這個函數指定級別了。
帶參數和不帶參數的類裝飾器有很大的不同。
init :不再接收被裝飾函數,而是接收傳入參數。
call :接收被裝飾函數,實現裝飾邏輯。
class logger(object):
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func): # 接受函數
def wrapper(*args, **kwargs):
print("[{level}]: the function {func}() is running..."\
.format(level=self.level, func=func.__name__))
func(*args, **kwargs)
return wrapper #返回函數
@logger(level='WARNING')
def say(something):
print("say {}!".format(something))
say("hello")
指定WARNING級別后的運行結果:
[WARNING]: the function say() is running…
say hello!
3. 無參裝飾器模板
其中wrapper功能:
1、調用原函數
2、為其增加新功能
def template(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
4. 有參裝飾器模板
def 有參裝飾器(x,y,z):
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
return outter
@有參裝飾器(1,y=2,z=3)
def 被裝飾對象():
pass

