python装饰器_python装饰器完全指南之一
設我們有一組函數(shù),它們有共同的錯誤處理方法,比如打印日志和記錄審計信息等。很顯然,在每一個函數(shù)中都重復這些邏輯是不恰當?shù)?#xff0c;它們應該被提煉到一個函數(shù)里,在這個函數(shù)的保護下,再調用我們的業(yè)務邏輯處理功能。
盡管錯誤處理可能占據(jù)代碼的主要部分,但業(yè)務邏輯才是程序的核心價值。因此,從代碼結構上看,錯誤處理應該處于可被忽略的非中心地帶。如果我們每次調用業(yè)務邏輯處理功能前,都要先顯式地從一個錯誤處理函數(shù)開始,這種寫法顯然是頭重腳輕,也會打斷代碼閱讀者的思緒。基于這些原因,開發(fā)語言引入了面向切面的編程(AOP):把與主業(yè)務無關的事情,放到代碼之外去做。
裝飾器是AOP編程中不可缺少的語法糖。通過裝飾器語法,可以使得程序更簡潔易讀。本文對裝飾器的基礎原理、一般寫法、corner case和常見場景進行了探討。
三個系列共三篇文章,可能是互聯(lián)網(wǎng)上最全、最深入的python裝飾器指南之一。
從一個最簡單的裝飾器開始
假設我們有一個功能函數(shù)(從現(xiàn)在開始,我們把被裝飾器修飾,完成業(yè)務邏輯的那些函數(shù)稱作功能函數(shù),以區(qū)別于裝飾器函數(shù)),出于調試目的,我們希望打印出它的參數(shù)及每次調用的返回值。
假設功能函數(shù)如下:
# block 1def buggy_incr_by(number): import random return random.randint(0,10) + number我們可以定義這樣一個函數(shù):
# block 2def snoop(func): def wrapper(number): print(f" >>> invoke {func.__name__} with parameter: {number}") result = func(number) return result print(f"<<< {func.__name__} returned {result}") return wrapper現(xiàn)在,運用裝飾器語法:
# block 3@snoopdef buggy_incr_by(number): import random return random.randint(0,10) + number# call and check the resultbuggy_incr_by(3)# --- output --->>> invoke buggy_incr_by with parameter: 3<<< buggy_incr_by returned 13裝飾器究竟是如何工作的?
現(xiàn)在我們來看一看這一切是如何發(fā)生的。
這里最基本的原理有:
1. 在python中,function(函數(shù))也是一種對象(當不帶括號引用時)。你可以任意選擇一個函數(shù)f,通過dir(f)來查看它有哪些屬性。
2. 在函數(shù)內也可以定義函數(shù),并返回這個定義的函數(shù)對象。這是因為根據(jù)原理1,函數(shù)本身也是對象。
3. 模塊加載器調用exec_module時,會查找和解析@語句,通過執(zhí)行 func = decorator(func),重新定義功能函數(shù)。
在上面的例子中,我們定義了裝飾器函數(shù)snoop,它接受一個規(guī)定好的參數(shù)(必須),即功能函數(shù)對象本身。decorator的主要功能是定義并返回一個函數(shù)對象(下面稱之為替換函數(shù))。這個函數(shù)對象中,完成我們需要的面向切面的功能,并且調用功能函數(shù),返回其返回值。
當上述代碼所在的模塊文件被importlib加載并執(zhí)行時,加載器(Loader)發(fā)現(xiàn)存在'@'語法糖,于是執(zhí)行:
# block 4buggy_incr_by = snoop(buggy_incr_by)結合snoop的代碼不難發(fā)現(xiàn),snoop將返回一個名為wrapper的函數(shù)對象(替換函數(shù)),賦值給buggy_incr_by,所以此后調用buggy_incr_by,實際上就是在調用這個wrapper。
下面是寫一個最簡單的裝飾器時的一般要訣:
1. 裝飾器decorator只接受一個形參(名字可以任意取),這個形參將模塊加載器調用exec_module時,從@注解的下一行函數(shù)的定義中找到被定義的函數(shù)對象傳入。見上一個代碼塊的說明。
2. 裝飾器的函數(shù)體必須定義并返回一個wrapper函數(shù)(名字可以任意取)。這個wrapper(替換)函數(shù)的簽名一般情況下等同于功能函數(shù)。例外情況在下文中敘述。
3. 在添加裝飾器注解(即'@'語法)時,不需要顯式地將功能函數(shù)參數(shù)傳給裝飾器,這將由模塊加載器自動完成。因此,如果裝飾器只有這一個參數(shù),注解中必須是不帶括號引用,見上面第2行。
4. 如果功能函數(shù)有返回值,則在wrapper的函數(shù)體中,也需要將返回值返回,參見block 2第6行。
通過上述分析,我們還有幾個重要的結論:
1. 裝飾器語法在模塊加載時就運行了,并且重新定義了功能函數(shù)的指向(即上述代碼中的wrapper)。
2. 在定義wrapper時,功能函數(shù)并沒有真正被調用,因此需要延遲綁定的參數(shù),比如self對象,此時是不存在的。
3. 在代碼的其它地方調用功能函數(shù)時,實際上是在調用上述wrapper,此時實現(xiàn)參數(shù)的綁定(即給形參賦值)。
找回丟失的調試信息
從前面的分析可以看出,功能函數(shù)在模塊加載過程中,實際上被替換成了wrapper函數(shù)。我們可以通過下面的測試來發(fā)現(xiàn)這一點:
print(buggy_incr_by.__name__)# --output---wrapper顯然buggy_incr_by已經(jīng)被替換了。但這里也暴露出一個問題:如果程序出錯,則在需要顯示棧信息的地方,則都會顯示為wrapper,而不是功能函數(shù)的名字。比如下面一例:
def snoop(func): def wrapper(number): print("passed in param is ", number) result = func(number) print("buggy_incr_by returned ", result) return wrapper@snoopdef buggy_incr_by(number): import random breakpoint() return random.randint(0,10) + numberbuggy_incr_by(3)我們在第11行放置了一個斷點,運行之后,我們查看堆棧信息如下:
-> command.run()-> self.more = self.interpreter.runsource(text, '', symbol) /home/.../code.py(74)runsource()-> self.runcode(code) /home/.../code.py(90)runcode()-> exec(code, self.locals) (14)() (4)wrapper()斷點設置在bugg_incr_by中,但顯示的最底層的函數(shù)名卻為wrapper,這會使得調試變困難,因此我們需要更正這一信息。
函數(shù)作為一種對象,它有以下元屬性:
# __module__, __name__, __qualname__, __doc__, __annotations__for name in ['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']: print(getattr(buggy_incr_by, name))# --output--__main__wrappersnoop..wrapperNone{}我們需要用功能函數(shù)的這些元屬性來改寫替換函數(shù)的相關屬性:
setattr(buggy_incr_by, '__name__', 'gime new name')for name in ['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']: print(getattr(buggy_incr_by, name))#--output--__main__gime new namesnoop..wrapperNone{}通過使用setattr,我們可以很容易替換掉這些信息。我們看到buggy_incr_by現(xiàn)在有了新的名字,即'gime new name'
不過,我們沒有必要親自去做這些瑣事。我們可以在代碼段block 2的第三行,即在wrapper之前,調用functools.wraps來為我們解決這個問題,這里functools.wrapper是另一個裝飾器:
import functoolsdef snoop(func): @functools.wraps(func) # wraps需要接收func參數(shù) def wrapper(number): print("passed in param is ", number) result = func(number) print("buggy_incr_by returned ", result) return wrapper@snoopdef buggy_incr_by(number): import random breakpoint() return random.randint(0,10) + numberbuggy_incr_by.__name__注意第3行的注釋。很顯然functools.wraps需要這個參數(shù),因為它要從func中獲取`__name__`,` __qualname__`, `__doc__`等信息,以便去更新下面的wraper。實際上,functools.wraps是接收了兩個函數(shù)對象作為參數(shù)。
總結
以上是生活随笔為你收集整理的python装饰器_python装饰器完全指南之一的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 2003报错_为什么不建议在
- 下一篇: activity 启动模式_Androi