Python異常處理詳解
入門示例
異常處理:try/except
對於索引查詢的操作,在索引越界搜尋的時候會報錯。例如:
>>> s="long"
>>> s[4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
所報的錯誤是IndexError。如果將索引查詢放在一個函式裡:
>>> def fetcher(obj,index): ... return obj[index]
那麼呼叫函式的時候,如果裡面的索引越界了,異常將彙報到函式呼叫者。
>>> fetcher(s,3)
'g'
>>> fetcher(s,4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in fetcher
IndexError: string index out of range
可以使用try/except來捕獲異常。作為入門示例,下面是簡單版的格式:
try: statement1 ... statementN except <ERRORTYPE>: ...statementS...
例如,try中是要監視正確執行與否的語句,ERRORTYPE是要監視的錯誤型別。
- 只要try中的任何一條語句丟擲了錯誤,try中該異常語句後面的語句都不會再執行;
- 如果丟擲的錯誤正好是except所監視的錯誤型別,就會執行statementS部分的語句;
- 如果異常正好被except捕獲(匹配)到了,程式在執行完statementS後會繼續執行下去,如果沒有捕獲到,程式將終止;
- 換句話說,except捕獲到錯誤後,相當於處理了這個錯誤,程式不會因為已經被處理過的錯誤而停止
例如捕獲上面的函式呼叫:
def fetcher(obj, index): return obj[index] s = "long" try: print(fetcher(s, 3) * 4) print(fetcher(s, 4) * 4) except IndexError: print("something wrong") print("after Exception, Continue")
輸出結果:
gggg
something wrong
after Exception, Continue
因為上面的fetcher(s, 4)
會丟擲異常,且正好匹配except監視的異常型別,所以輸出something wrong
,異常被處理之後,程式繼續執行,即try/except後面的print()。
異常處理:try/finally
finally是try之後一定會執行的語句段落。可以結合except一起使用。
try:
statement1
...
statementN
finally:
...statementF...
try:
statement1
...
statementN
except <ERRORTYPE>:
...statementS...
finally:
...statementF...
不論try中的語句是否出現異常,不論except是否捕獲到對應的異常,finally都會執行:
- 如果異常沒有被捕獲,則在執行finally之後程式退出
- 如果異常被except捕獲,則執行完except的語句之後執行finally的語句,然後程式繼續執行下去
一般來說,finally中都會用來做程式善後清理工作。
例如:
def fetcher(obj, index):
return obj[index]
s = "long"
try:
print(fetcher(s, 3) * 4)
print(fetcher(s, 4) * 4)
except IndexError:
print("something wrong")
finally:
print("in finally")
print("after Exception, Continue")
輸出:
gggg
something wrong
in finally
after Exception, Continue
如果把except那段程式碼刪掉,得到的結果將是:
gggg
in finally # 輸出了finally的內容
Traceback (most recent call last):
File "g:/pycode/list.py", line 8, in <module>
print(fetcher(s, 4) * 4)
File "g:/pycode/list.py", line 2, in fetcher
return obj[index]
IndexError: string index out of range
產生異常:raise和assert
使用raise或assert可以主動生成異常情況。其中raise可以直接丟擲某個異常,assert需要通過布林值來判斷,然後再丟擲給定的錯誤。
例如,在函式裡做個沒什麼用的的判斷,用來演示raise:
def fetcher(obj, index):
if index >= len(obj):
raise IndexError
return obj[index]
這和直接索引越界是以一樣的。上面raise丟擲的異常IndexError是一個內建異常,可以直接引用這些內建異常。稍後會演示如何自定義自己的異常。
丟擲異常後,就可以按照前面介紹的try來處理異常。
assert是一種斷言,在計算機語言中表示:如果斷言條件為真就跳過,如果為假就丟擲異常資訊。它可以自定義異常資訊。
例如:
def fetcher(obj, index):
assert index < len(obj), "one exception"
return obj[index]
很多時候會直接在assert中使用False、True的布林值進行程式的除錯。
assert True, "assert not hit"
assert False, "assert hit"
自定義異常
python中的異常是通過類來定義的,而且所有的異常類都繼承自Exception類,而Exception又繼承自BaseException(這個類不能直接作為其它異常類的父類)。所以自定義異常的時候,也要繼承Exception,當然,繼承某個中間異常類也可以。
例如,定義索引越界的異常類,注意這個類中直接pass,但因為繼承了Exception,它仍然會有異常資訊。
class MyIndexError(Exception):
pass
例如,判斷字母是否是大寫,如果是,就拋異常:
def fetcher(obj,index):
if index >= len(obj):
raise MyIndexError
return obj[index]
測試一下:
s = "long"
print(fetcher(s, 3) * 4)
print(fetcher(s, 4) * 4)
結果:
gggg
Traceback (most recent call last):
File "g:/pycode/list.py", line 12, in <module>
print(fetcher(s, 4) * 4)
File "g:/pycode/list.py", line 6, in fetcher
raise MyIndexError
__main__.MyIndexError
需要注意,因為異常類都繼承字Exception,except監視Exception異常的時候,也會匹配其它的異常。更標準地說,監視異常父類,也會捕獲到這個類的子類異常。
如何看丟擲的異常
看異常資訊是最基本的能力。例如,下面的這段程式碼會報除0錯誤:
def a(x, y):
return x/y
def b(x):
print(a(x, 0))
b(1)
執行時,報錯資訊如下:
Traceback (most recent call last):
File "g:/pycode/list.py", line 7, in <module>
b(1)
File "g:/pycode/list.py", line 5, in b
print(a(x, 0))
File "g:/pycode/list.py", line 2, in a
return x/y
ZeroDivisionError: division by zero
這個堆疊跟蹤資訊中已經明確說明了(most recent call last)
,說明最近產生異常的呼叫在最上面,也就是第7行。上面的整個過程是這樣的:第7行出錯,它是因為第5行的程式碼引起的,而第5行之所以錯是第2行的原始碼引起的。
所以,從最底部可以看到最終是因為什麼而丟擲異常,從最頂部可以看到是執行到哪一句出錯。
深入異常處理
try/except/else/finally
try:
<statements>
except <name1>: # 捕獲到名為name1的異常
<statements>
except (name2, name3): # 捕獲到name2或name3任一異常
<statements>
except <name4> as <data>: # 捕獲name4異常,並獲取異常的示例
<statements>
except: # 以上異常都不匹配時
<statements>
else: # 沒有產生異常時
<statements>
finally: # 一定會執行的
<statements>
注意,當丟擲的異常無法被匹配時,將歸類於空的except:
,但這是很危險的行為,因為很多時候的異常是必然的,比如某些退出操作、記憶體不足、Ctrl+C等等,而這些都會被捕獲。與之大致等價的是捕獲異常類的"偽"祖先類Exception
,即except Exception:
,它和空異常匹配類似,但能解決不少不應該匹配的異常。但使用Exception依然是危險的,能不用盡量不用。
如果一個異常既能被name1匹配,又能被name2匹配,則先匹配到的處理這個異常。
通過as關鍵字可以將except捕獲到的異常物件賦值給data變數。用法稍後會解釋,現在需要知道的是,在python 3.x中,變數data只在當前的except塊範圍內有效,出了範圍就會被回收。如果想要保留異常物件,可以將data賦值給一個變數。例如下面的b在出了try範圍都有效,但是a在這個except之後就無效了。
except Exception as a:
print(a)
b=a
通過else分句可以知道,這段try程式碼中沒有出現任何異常。否則就不會執行到else分句。
raise
raise用於手動觸發一個異常。而每一種異常都是一個異常類,所以觸發實際上是觸發一個異常類的例項物件。
raise <instance> # 直接觸發一個異常類的物件
raise <class> # 構建此處所給類的一個異常物件並觸發
raise # 觸發最近觸發的異常
raise <2> from <1> # 將<1>的異常附加在<2>上
其中第二種形式,raise會根據給定類不傳遞任何引數地自動構建一個異常物件,並觸發這個異常物件。第三種直接觸發最近觸發的異常物件,這在傳播異常的時候很有用。
例如,下面兩種方式實際上是等價的,只不過第一種方式傳遞的是類,raise會隱式地自動建立這個異常類的例項物件。
raise IndexError
raise IndexError()
可以為異常類構建例項時指定點引數資訊,這些引數會儲存到名為args的元組。例如:
try:
raise IndexError("something wrong")
except Exception as E:
print(E.args)
輸出:
('something wrong',)
不僅如此,只要是異常類或異常物件,不管它們的存在形式如何,都可以放在raise中。例如:
err = IndexErro()
raise err
errs = [IndexError, TypeError]
raise errs[0]
對於第三種raise形式,它主要用來傳播異常,一般用在except程式碼段中。例如:
try:
raise IndexError("aaaaa")
except IndexError:
print("something wrong")
raise
因為異常被except捕獲後,就表示這個異常已經處理過了,程式會跳轉到finally或整個try塊的尾部繼續執行下去。但是如果不想讓程式繼續執行,而是僅僅只是想知道發生了這個異常,並做一番處理,然後繼續向上觸發異常。這就是異常傳播。
因為實際觸發的異常都是類的例項物件,所以它有屬性。而且,可以通過在except中使用as來將物件賦值給變數:
try:
1/0
except Exception as a:
print(a)
變數a在出了except的範圍就失效,所以可以將它保留給一個不會失效的變數:
try:
1/0
except Exception as a:
print(a)
b=a
print(b)
如果在一個except中觸發了另一個異常,會造成異常鏈:
try:
1/0
except Exception as E:
raise TypeError('Bad')
將會報告兩個異常,並提示處理異常E的時候,觸發了另一個異常TypeError。
Traceback (most recent call last):
File "g:/pycode/list.py", line 2, in <module>
1/0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "g:/pycode/list.py", line 4, in <module>
raise TypeError('Bad')
TypeError: Bad
使用from關鍵字,可以讓關係更加明確。
try:
1/0
except Exception as E:
raise TypeError('Bad') from E
下面是錯誤報告:
Traceback (most recent call last):
File "g:/pycode/list.py", line 2, in <module>
1/0
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "g:/pycode/list.py", line 4, in <module>
raise TypeError('Bad') from E
TypeError: Bad
實際上,使用from關鍵字的時候,會將E的異常物件附加到TypeError的__cause__
屬性上。
但無論如何,這裡都觸發了多個異常。在python 3.3版本,可以使用from None
的方式來掩蓋異常的來源,也就是禁止輸出異常E,停止異常鏈:
try:
1/0
except Exception as E:
raise TypeError('Bad') from None
錯誤報告如下:
Traceback (most recent call last):
File "g:/pycode/list.py", line 4, in <module>
raise TypeError('Bad') from None
TypeError: Bad
可見,異常資訊中少了很多內容。
assert
assert斷言常用於除錯。用法如下:
assert test, data
它實際上等價於是條件判斷的raise。它等價於下面的方式:
if __debug__:
if not test:
raise AssertionError(data)
如果條件test的測試為真,就跳過,否則就丟擲異常。這個異常是通過AssertionError類構造的,構造異常物件的引數是data。data會放進名為args的元組屬性中。
try:
assert False,"something wrong"
except Exception as E:
print(E.args)
同樣,assert產生的是名為AssertionError的異常,如果不捕獲這個AssertionError異常,程式將會終止。
除了除錯,assert還偶爾用來判斷必要的條件,不滿足條件就異常,以便讓程式更加健壯。例如:
def f(x):
assert x >= 0, "x must great or equal than 0"
return x ** 2
print(f(2))
print(f(0))
print(f(-2)) # 觸發AssertionError異常
需要注意的是,寫assert的測試條件時,測試結果為假才觸發異常。所以,應該以if not true的角度去考慮條件,或者以unless的角度考慮。或者說,後面觸發的異常資訊,和測試條件應該是正相關的,例如示例中異常資訊的說法是x必須大於等於0,和測試條件x >= 0
是正相關的。
assert還常用於父類方法的某些方法中,這些方法要求子類必須重寫父類的方法。於是:
class cls:
...
def f(self):
assert False, "you must override method: f"
此外,assert不應該用來觸發那些python早已經定義好的異常。例如索引越界、型別錯誤等等。這些python已經定義好的異常,我們再去用AssertionError觸發,這是完全多餘的。例如:
def f(obj,index):
assert index > len(obj), "IndexError"
return obj[index]
sys.exc_info()
該函式用來收集正在處理的異常的資訊。
它返回一個包含3個值的元組(type, value, traceback)
,它們是當前正在處理的異常的資訊。如果沒有正在處理的異常,則返回3個None組成的元組。
其中:
- type表示正在處理的異常類
- value表示正在處理的異常例項
- traceback表示一個棧空間的回撥物件(參考官方手冊traceback object)
看一個示例即可知道。
class General(Exception):pass
def raise0():
x = General()
raise x
try:
raise0()
except Exception:
import sys
print(sys.exc_info())
執行結果:
(<class '__main__.General'>, General(), <traceback object at 0x0388F2B0>)
結果很明顯,第一個返回值是異常類General,第二個返回值是丟擲的異常類的例項物件,第三個返回值是traceback物件。
實際上,當需要獲取當前處理的異常類時,還可以通過異常物件的__class__
來獲取,因為異常物件可以在except/as中賦值給變數:
class General(Exception):pass
def raise0():
x = General()
raise x
try:
raise0()
except Exception as E:
import sys
print(sys.exc_info()[0])
print(E.__class__)
它們的的結果是完全一樣的:
<class '__main__.General'>
<class '__main__.General'>
什麼時候要獲取異常類的資訊?當except所監視的異常類比較大範圍,同時又想知道具體的異常類。比如,except:
或except Exception:
這兩種監視的異常範圍都非常大,前者會監視BaseException,也就是python的所有異常,後者監視的是Exception,也就是python的所有普通的異常。正因為監視範圍太大,導致不知道具體是丟擲的是哪個異常。
區分異常和錯誤
錯誤都是異常,但異常並不一定都是錯誤。
很常見的,檔案結尾的EOF在各種語言中它都定義為異常,是異常就能被觸發捕獲,但在邏輯上卻不認為它是錯誤。
除此之外,還有作業系統的異常,比如sys.exit()引發的SystemeExit異常,ctrl+c引發的的中斷異常KeyboardInterrupt都屬於異常,但它們和普通的異常不一樣。而且python中的普通異常都繼承字Exception類,但SystemExit卻並非它的子類,而是BaseException的子類。所以能通過空的except:
捕獲到它,卻不能通過except Exception:
來捕獲。
異常類的繼承
所有異常類都繼承自Exception,要編寫自定義的異常時,要麼直接繼承該類,要麼繼承該類的某個子類。
例如,下面定義三個異常類,General類繼承Exception,另外兩個繼承General類,表示這兩個是特定的、更具體的異常類。
class General(Exception):pass
class Specific1(General): pass
class Specific2(General): pass
def raise0():
x = General()
raise x
def raise1():
x = Specific1()
raise x
def raise2():
x = Specific2()
raise x
測試下:
for func in (raise0, raise1, raise2):
try:
func()
except General as E:
import sys
print("caught: ", E.__class__)
執行結果:
caught: <class '__main__.General'>
caught: <class '__main__.Specific1'>
caught: <class '__main__.Specific2'>
前面說過,except監視父類異常的時候,也會捕獲該類的子類異常。正如這裡監視的是Gereral類,但觸發了Specific子類異常也會被捕獲。
異常類的巢狀
這是非常常見的陷阱。有兩種異常巢狀的方式:try的巢狀;程式碼塊的異常巢狀(比如函式巢狀)。無論是哪種巢狀模式,異常都只在最近(或者說是最內層)的程式碼塊中被處理,但是finally塊是所有try都會執行的。
第一種try的巢狀模式:
try:
try:
(1)
except xxx:
(2)
finally:
(3)
except yyy:
...
finally:
(4)
如果在(1)處丟擲了異常,無論yyy是否匹配這個異常,只要xxx能匹配這個異常,就會執行(2)。但(3)、(4)這兩個finally都會執行。
第二種程式碼塊巢狀,常見的是函式呼叫的巢狀,這種情況可能會比較隱式。例如:
def action2():
print(1 + [])
def action1():
try:
action2()
except TypeError:
print('inner try')
try:
action1()
except TypeError:
print('outer try')
執行結果:
inner try
上面的action2()會丟擲一個TypeError的異常。在action1()中用了try包圍action2()的呼叫,於是action2()的異常彙報給action1()層,然後被捕獲。
但是在最外面,使用try包圍action1()的呼叫,看上去異常也會被捕獲,但實際上並不會,因為在action2()中就已經通過except處理好了異常,而處理過的異常將不再是異常,不會再觸發外層的異常,所以上面不會輸出"outer try"。
except應該捕獲哪些異常
在考慮異常捕獲的時候,需要注意幾點:
- except監視的範圍別太大了
- except監視的範圍別太小了
- 有些異常本就該讓它中斷程式的執行,不要去捕獲它
第三點很容易理解,有些異常會導致程式無法進行後續的執行,改中斷還是得中斷。
對於第一點,可能常用的大範圍異常監視就是下面兩種方式:
except:
except Exception:
這兩種方式監視的範圍都太大了,比如有些不想處理的異常也會被它們監視到。更糟糕的可能是本該彙報給上層的異常,結果卻被這種大範圍捕獲了。例如:
def func():
try:
...
except:
...
try:
func()
except IndexErro:
...
本來是想在外層的try中明確捕獲func觸發的IndexError異常的,但是func()內卻使用了空的except:
,使得異常直接在這裡被處理,外層的try永遠也捕獲不到任何該函式的異常。
關於第二點,不應該監視範圍太小的異常。範圍小,意味著監視的異常太過具體,太過細緻,這種監視方式雖然精確,但卻不利於維護。例如E1異常類有2個子異常類E2、E3,在程式碼中監視了E2、E3,但如果未來又添加了一個E1的子異常類E4,那麼又得去改程式碼讓它監視E4。如果程式碼是寫給自己用的倒無所謂,但如果像通用模組一樣交給別人用的,這意味著讓別的模組使用者也去改程式碼。
自定義異常類
在前面設計異常類的時候,總是使用pass跳過類程式碼體。但卻仍然能使用這個類作為異常類,因為它繼承了Exception,在Exception中有相關程式碼可以輸出異常資訊。
前面說過,在構造異常類的時候可以傳遞引數,它們會放進異常例項物件的args屬性中:
try:
raise IndexError("something wrong")
except Exception as E:
print(E.args)
try:
assert False,"something wrong too"
except Exception as E:
print(E.args)
I = IndexError('text')
print(I.args)
對於使用者自定義的類,也一樣如此:
class MyError(Exception):pass
try:
raise MyError('something wrong')
except MyError as E:
print(E.args)
不僅如此,雖然異常例項物件是一個物件,但如果直接輸出例項物件,那麼得到的結果將是給定的異常資訊,只不過它不在元組中。
I = IndexError("index wrong")
print(I) # 輸出"index wrong"
很容易想到,這是因為Exception類中重寫了__str__
或者__repr__
中的某一個或兩個都重寫了。
自定義異常輸出
於是,自定義異常類的時候,也可以重寫這兩個中的一個,從而可以定製屬於自己的異常類的輸出資訊。一般來說只重寫__str__
,因為Exception中也是重寫該類,且它的優先順序高於__repr__
。
例如下面自定義的異常類。當然,這個示例的效果非常簡陋,但已足夠說明問題。
class MyError(Exception):
def __str__(self):
return 'output this message for something wrong'
try:
raise MyError("hahhaha")
except MyError as E:
print(E)
輸出結果:
output this message for something wrong
提供構造方法
自定義異常類的時候,可以重寫構造方法__init__()
,這樣raise異常的時候,可以指定構造的資料。而且更進一步的,還可以重寫__str__
來自定義異常輸出。
例如,格式化檔案的程式中定義一個異常類,用來提示解析到哪個檔案的哪一行出錯。
class MyError(Exception):
def __init__(self,line,file):
self.line = line
self.file = file
def __str__(self):
return "format failed: %s at %s" % (self.file, self.line)
def format():
...
raise MyError(42, "a.json")
...
try:
format()
except MyError as E:
print(E)
提供異常類的其它方法
異常類既然是類,說明它可以像普通類一樣拿來應用。比如新增其它類方法。
例如,可以將異常資訊寫入到檔案中。只需提供一個向檔案寫入的方法,並在except的語句塊中呼叫這個方法即可。
class MyError(Exception):
def __init__(self, line, file):
self.line = line
self.file = file
def logerr(self):
with open('Error.log', 'a') as logfile:
print(self, file=logfile)
def __str__(self):
return "format failed: %s at %s" % (self.file, self.line)
def format():
raise MyError(42, "a.json")
try:
format()
except MyError as E:
E.logerr()