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