動態(tài)捕獲Python異常
在討論動態(tài)捕獲異常時讓我大吃一驚的是,可以讓我找到隱藏的Bug和樂趣...
有問題的代碼
下面的代碼來自一個產(chǎn)品中看起來是好的抽象代碼 - slightly(!) .這是調(diào)用一些統(tǒng)計數(shù)據(jù)的函數(shù),然后進(jìn)行處理 . 首先是用socket連接獲取一個值,可能發(fā)生了socket錯誤.由于統(tǒng)計數(shù)據(jù)在系統(tǒng)中不是至關(guān)重要的,我們只是記一下日志錯誤并繼續(xù)往下走.
(請注意,這篇文章我使用doctest測試的 - 這代表代碼可以運(yùn)行!)
- >>> def get_stats():
- ... pass
- ...
- >>> def do_something_with_stats(stats):
- ... pass
- ...
- >>> try:
- ... stats = get_stats()
- ... except socket.error:
- ... logging.warning("Can't get statistics")
- ... else:
- ... do_something_with_stats(stats)
查找
我們測試時并沒有發(fā)現(xiàn)不妥, 但實(shí)際上我們注意到靜態(tài)分析報告顯示一個問題:
- $ flake8 filename.py
- filename.py:351:1: F821 undefined name 'socket'
- filename.py:352:1: F821 undefined name 'logging'
顯然是我們沒測試,這個問題是代碼中我們沒有引用socket 和 logging 兩個模塊.使我感到驚奇的是,這并沒有預(yù)先拋出NameError錯,我以為它會查找這些異常語句中的一些名詞,如它需要捕捉這些異常,它需要知道些什么呢!
事實(shí)證明并非如此,異常語句的查找是延遲完成的,只是評估時拋出異常. 不只是名稱延遲查找,也可以定制顯示聲明異常做為'參數(shù)(argument)'.
這可能是好事,壞事,或者是令人厭惡的.
好事(上段中提到的)
異常參數(shù)可以以任意形式數(shù)值傳遞. 這樣就允許了異常的動態(tài)參數(shù)被捕獲.
- >>> def do_something():
- ... blob
- ...
- >>> def attempt(action, ignore_spec):
- ... try:
- ... action()
- ... except ignore_spec:
- ... pass
- ...
- >>> attempt(do_something, ignore_spec=(NameError, TypeError))
- >>> attempt(do_something, ignore_spec=TypeError)
- Traceback (most recent call last):
- ...
- NameError: global name 'blob' is not defined
壞事(上段中提到的)
這種明顯的弊端就是異常參數(shù)中的錯誤通常只有在異常觸發(fā)之后才會被注意到,不過為時已晚.當(dāng)用異常去捕獲不常見的事件時(例如:以寫方式打開文件失敗), 除非做個一個特定的測試用例,否則只有當(dāng)一個異常(或者任何異常)被觸發(fā)的時候才會知道, 屆時記錄下來并且查看是否有匹配的異常, 并且拋出它自己的錯誤異常 - 這是一個NameError通常所做的事情.
- >>> def do_something():
- ... return 1, 2
- ...
- >>> try:
- ... a, b = do_something()
- ... except ValuError: # oops - someone can't type
- ... print("Oops")
- ... else:
- ... print("OK!") # we are 'ok' until do_something returns a triple...
- OK!
令人討厭的(上段中提到的)
- >>> try:
- ... TypeError = ZeroDivisionError # now why would we do this...?!
- ... 1 / 0
- ... except TypeError:
- ... print("Caught!")
- ... else:
- ... print("ok")
- ...
- Caught!
不僅僅是異常參數(shù)通過名稱查找, - 其它的表達(dá)式也是這樣工作的:
- >>> try:
- ... 1 / 0
- ... except eval(''.join('Zero Division Error'.split())):
- ... print("Caught!")
- ... else:
- ... print("ok")
- ...
- Caught!
異常參數(shù)不僅僅只能在運(yùn)行時確定,它甚至可以使用在生命周期內(nèi)的異常的信息. 以下是一個比較費(fèi)解的方式來捕捉拋出的異常 - 但也只能如此了:
- >>> import sys
- >>> def current_exc_type():
- ... return sys.exc_info()[0]
- ...
- >>> try:
- ... blob
- ... except current_exc_type():
- ... print ("Got you!")
- ...
- Got you!
很明顯這才是我們真正要尋找的當(dāng)我們寫異常處理程序時, 我們應(yīng)該首先想到的就是這種
(字節(jié))代碼
為了確認(rèn)它是如何在異常處理工作中出現(xiàn)的,我在一個異常的例子中運(yùn)行 dis.dis(). (注意 這里的分解是在Python2.7 下 - 不同的字節(jié)碼是Python 3.3下產(chǎn)生的,但這基本上是類似的):
- >>> import dis
- >>> def x():
- ... try:
- ... pass
- ... except Blobbity:
- ... print("bad")
- ... else:
- ... print("good")
- ...
- >>> dis.dis(x) # doctest: +NORMALIZE_WHITESPACE
- 2 0 SETUP_EXCEPT 4 (to 7)
- <BLANKLINE>
- 3 3 POP_BLOCK
- 4 JUMP_FORWARD 22 (to 29)
- <BLANKLINE>
- 4 >> 7 DUP_TOP
- 8 LOAD_GLOBAL 0 (Blobbity)
- 11 COMPARE_OP 10 (exception match)
- 14 POP_JUMP_IF_FALSE 28
- 17 POP_TOP
- 18 POP_TOP
- 19 POP_TOP
- <BLANKLINE>
- 5 20 LOAD_CONST 1 ('bad')
- 23 PRINT_ITEM
- 24 PRINT_NEWLINE
- 25 JUMP_FORWARD 6 (to 34)
- >> 28 END_FINALLY
- <BLANKLINE>
- 7 >> 29 LOAD_CONST 2 ('good')
- 32 PRINT_ITEM
- 33 PRINT_NEWLINE
- >> 34 LOAD_CONST 0 (None)
- 37 RETURN_VALUE
這顯示出了我原來預(yù)期的問題(issue). 異常處理"看起來"完全是按照Python內(nèi)部機(jī)制在運(yùn)行. 這一步完全沒有必要知道關(guān)于后續(xù)的異常“捕獲”語句, 并且如果沒有異常拋出它們將被完全忽略了.SETUP_EXCEPT并不關(guān)心發(fā)生了什么, 僅僅是如果發(fā)生了異常, ***個處理程序應(yīng)該被評估,然后第二個,以此類推.
每個處理程序都有兩部分組成: 獲得一個異常的規(guī)則, 和剛剛拋出的異常進(jìn)行對比. 一切都是延遲的, 一切看起來正如對你的逐行的代碼的預(yù)期一樣, 從解釋器的角度來考慮. 沒有任何聰明的事情發(fā)生了,只是突然使得它看起來非常聰明.
總結(jié)
雖然這種動態(tài)的異常參數(shù)讓我大吃一驚, 但是這當(dāng)中包含很多有趣的應(yīng)用. 當(dāng)然去實(shí)現(xiàn)它們當(dāng)中的許多或許是個餿主意,呵呵
有時并不能總是憑直覺來確認(rèn)有多少Python特性的支持 - 例如 在類作用域內(nèi) 表達(dá)式和聲明都是被顯式接受的, (而不是函數(shù), 方法, 全局作用域),但是并不是所有的都是如此靈活的. 雖然(我認(rèn)為)那將是十分美好的, 表達(dá)式被禁止應(yīng)用于裝飾器 - 以下是Python語法錯誤:
- @(lambda fn: fn)
- def x():
- pass
這個是嘗試動態(tài)異常參數(shù)通過給定類型傳遞給***個異常的例子, 靜靜的忍受重復(fù)的異常:
- >>> class Pushover(object):
- ... exc_spec = set()
- ...
- ... def attempt(self, action):
- ... try:
- ... return action()
- ... except tuple(self.exc_spec):
- ... pass
- ... except BaseException as e:
- ... self.exc_spec.add(e.__class__)
- ... raise
- ...
- >>> pushover = Pushover()
- >>>
- >>> for _ in range(4):
- ... try:
- ... pushover.attempt(lambda: 1 / 0)
- ... except:
- ... print ("Boo")
- ... else:
- ... print ("Yay!")
- Boo
- Yay!
- Yay!
- Yay!
英文原文:The Dynamics of Catching Exceptions in Python
譯文連接:http://www.oschina.net/translate/the-dynamics-of-catching-exceptions-in-python