在討論動態捕獲異常時讓我大吃一驚的是,可以讓我找到隱藏的Bug和樂趣...
有問題的程式碼
下面的程式碼來自一個產品中看起來是好的抽象程式碼 - slightly(!) .這是呼叫一些統計資料的函式,然後進行處理 . 首先是用socket連線獲取一個值,可能發生了socket錯誤.由於統計資料在系統中不是至關重要的,我們只是記一下日誌錯誤並繼續往下走.
(請注意,這篇文章我使用doctest測試的 - 這代表程式碼可以執行!)
>>> 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)
查詢
我們測試時並沒有發現不妥, 但實際上我們注意到靜態分析報告顯示一個問題:
$ flake8 filename.py
filename.py:351:1: F821 undefined name 'socket'
filename.py:352:1: F821 undefined name 'logging'
顯然是我們沒測試,這個問題是程式碼中我們沒有引用socket 和 logging 兩個模組.使我感到驚奇的是,這並沒有預先丟擲NameError錯,我以為它會查詢這些異常語句中的一些名詞,如它需要捕捉這些異常,它需要知道些什麼呢!
事實證明並非如此,異常語句的查詢是延遲完成的,只是評估時丟擲異常. 不只是名稱延遲查詢,也可以定製顯示宣告異常做為'引數(argument)'.
這可能是好事,壞事,或者是令人厭惡的.
好事(上段中提到的)
異常引數可以以任意形式數值傳遞. 這樣就允許了異常的動態引數被捕獲.
>>> 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
壞事(上段中提到的)
這種明顯的弊端就是異常引數中的錯誤通常只有在異常觸發之後才會被注意到,不過為時已晚.當用異常去捕獲不常見的事件時(例如:以寫方式開啟檔案失敗),除非做個一個特定的測試用例,否則只有當一個異常(或者任何異常)被觸發的時候才會知道, 屆時記錄下來並且檢視是否有匹配的異常, 並且丟擲它自己的錯誤異常 - 這是一個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!
不僅僅是異常引數透過名稱查詢, - 其它的表示式也是這樣工作的:
>>> try:
... 1 / 0
... except eval(''.join('Zero Division Error'.split())):
... print("Caught!")
... else:
... print("ok")
...
Caught!
異常引數不僅僅只能在執行時確定,它甚至可以使用在生命週期內的異常的資訊. 以下是一個比較費解的方式來捕捉丟擲的異常 - 但也只能如此了:
>>> import sys
>>> def current_exc_type():
... return sys.exc_info()[0]
...
>>> try:
... blob
... except current_exc_type():
... print ("Got you!")
...
Got you!
很明顯這才是我們真正要尋找的當我們寫異常處理程式時, 我們應該首先想到的就是這種
(位元組)程式碼
為了確認它是如何在異常處理工作中出現的,我在一個異常的例子中執行 dis.dis(). (注意 這裡的分解是在Python2.7 下 - 不同的位元組碼是Python 3.3下產生的,但這基本上是類似的):
>>> 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
這顯示出了我原來預期的問題(issue). 異常處理"看起來"完全是按照Python內部機制在執行. 這一步完全沒有必要知道關於後續的異常“捕獲”語句, 並且如果沒有異常丟擲它們將被完全忽略了.SETUP_EXCEPT並不關心發生了什麼, 僅僅是如果發生了異常, 第一個處理程式應該被評估,然後第二個,以此類推.
每個處理程式都有兩部分組成: 獲得一個異常的規則, 和剛剛丟擲的異常進行對比. 一切都是延遲的, 一切看起來正如對你的逐行的程式碼的預期一樣, 從直譯器的角度來考慮. 沒有任務聰明的事情發生了,只是突然使得它看起來非常聰明.
總結
雖然這種動態的異常引數讓我大吃一驚, 但是這當中包含很多有趣的應用. 當然去實現它們當中的許多或許是個餿主意,呵呵
有時並不能總是憑直覺來確認有多少Python特性的支援 - 例如 在類作用域內 表示式和宣告都是被顯式接受的, (而不是函式, 方法, 全域性作用域),但是並不是所有的都是如此靈活的. 雖然(我認為)那將是十分美好的, 表示式被禁止應用於裝飾器 - 以下是Python語法錯誤:
@(lambda fn: fn)
def x():
pass
這個是嘗試動態異常引數透過給定型別傳遞給第一個異常的例子, 靜靜的忍受重複的異常:
>>> 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!