一、簡介
Python最強大的結(jié)構(gòu)之一就是它的異常處理能力,所有的標準異常都使用類來實現(xiàn),都是基類Exception的成員,都從基類Exception繼承,而且都在exceptions模塊中定義。Python自動將所有異常名稱放在內(nèi)建命名空間中,所以程序不必導(dǎo)入exceptions模塊即可使用異常。一旦引發(fā)而且沒有捕捉SystemExit異常,程序執(zhí)行就會終止。異常的處理過程、如何引發(fā)或拋出異常及如何構(gòu)建自己的異常類都是需要深入理解的。
二、詳解
1、什么是異常
(1)錯誤
從軟件方面來說,錯誤是語法或邏輯上的。語法錯誤指示軟件的結(jié)構(gòu)上有錯誤,導(dǎo)致不能被解釋器解釋或編譯器無法編譯。這些錯誤必須在程序執(zhí)行前糾正。邏輯錯誤可能是由于不完整或是不合法的輸入所致,在其他情況下,還可能是邏輯無法生成、計算或是輸出結(jié)果需要的過程無法執(zhí)行,這些錯誤通常分別被稱為域錯誤和范圍錯誤。
當(dāng)Python檢測到一個錯誤時,解釋器就會指出當(dāng)前流已經(jīng)無法繼續(xù)執(zhí)行下去。這時候就出現(xiàn)了異常。
(2)異常
對異常的最好描述是:它是因為程序出現(xiàn)了錯誤而在正常控制流以外采取的行為。這個行為又分為兩個階段:首先是引起異常發(fā)生的錯誤,然后是檢測和采取可能的措施階段。
第一個階段是在發(fā)生了一個異常條件(有時候也叫做例外的條件)后發(fā)生的。只要檢測到錯誤并且意識到異常條件,解釋器會引發(fā)一個異常。引發(fā)也可以叫做觸發(fā)、引發(fā)或者生成。 解釋器通過它通知當(dāng)前控制流有錯誤發(fā)生,Python也允許程序員自己引發(fā)異常。第二階段是:無論是 Python 解釋器還是程序員引發(fā)的,異常就是錯誤發(fā)生的信號, 當(dāng)前流將被打斷,用來處理這個錯誤并采取相應(yīng)的操作。對異常的處理發(fā)生在第二階段, 異常引發(fā)后,可以調(diào)用很多不同的操作。可以是忽略錯誤(記錄錯誤但不采取任何措施,采取補救措施后終止程序),或是減輕問題的影響后設(shè)法繼續(xù)執(zhí)行程序。所有的這些操作都代表一種繼續(xù),或是控制的分支。關(guān)鍵是程序員在錯誤發(fā)生時可以指示程序如何執(zhí)行。
Python采用了"try/嘗試"塊和"catching/捕獲"塊的概念,而且它在異常處理方面更有"紀律性"。可以為不同的異常創(chuàng)建不同的處理器, 而不是盲目地創(chuàng)建一個"catch-all/捕獲所有"的代碼。
2、Python中的異常
不管是通過Python解釋器執(zhí)行還是標準的腳本執(zhí)行,所有的錯誤都符合相似的格式, 這提供了一個一致的錯誤接口。所有錯誤,無論是語意上的還是邏輯上的,都是由于和Python解釋器不相容導(dǎo)致的,其后果就是引發(fā)異常。
NameError:嘗試訪問一個未申明的變量, 任何可訪問的變量必須在名稱空間里列出, 訪問變量需要由解釋器進行搜索,如果請求的名字沒有在任何名稱空間里找到,那么將會生成一個NameError異常。
ZeroDivisionError:除數(shù)為零;SyntaxError:Python 解釋器語法錯誤;IndexError:請求的索引超出序列范圍;KeyError:請求一個不存在的字典關(guān)鍵字;IOError:輸入/輸出錯誤;AttributeError:嘗試訪問未知的對象屬性。
3、檢測和處理異常
異常可以通過 try 語句來檢測,任何在try語句塊里的代碼都會被監(jiān)測, 檢查有無異常發(fā)生。try 語句有兩種主要形式: try-except和try-finally 。這兩個語句是互斥的, 即只能使用其中的一種。一個try語句可以對應(yīng)一個或多個except子句,但只能對應(yīng)一個finally子句,或是一個try-except-finally復(fù)合語句。可以使用try-except語句檢測和處理異常,也可以添加一個可選的else子句處理沒有探測到異常的時執(zhí)行的代碼,而try-finally只允許檢測異常并做一些必要的清除工作(無論發(fā)生錯誤與否)。
(1)try-except語句
try-except 語句(以及其更復(fù)雜的形式)定義了進行異常監(jiān)控的一段代碼,并且提供了處理異常的機制。
try-except 語句語法:
try:
try_suite # watch for exceptions here監(jiān)控這里的異常
except Exception[, reason]: # exception-handling code異常處理代碼
except_suite
在程序運行時,解釋器嘗試執(zhí)行try塊里的所有代碼,如果代碼塊完成后沒有異常發(fā)生,執(zhí)行流就會忽略except語句繼續(xù)執(zhí)行。而當(dāng)except語句所指定的異常發(fā)生后,會保存了錯誤的原因,控制流立即跳轉(zhuǎn)到對應(yīng)的處理器(try子句的剩余語句將被忽略)。
(2)封裝內(nèi)建函數(shù)
交互操作:把一個用字符串表示的數(shù)值轉(zhuǎn)換為正確的數(shù)值表示形式,其中float()增加了把字符串表示的數(shù)值轉(zhuǎn)換為浮點數(shù)的功能,替換以前的string.atof()。 float()對參數(shù)要求嚴格,例如如果參數(shù)的類型正確(字符串),其值不可轉(zhuǎn)換為浮點數(shù),那么將引發(fā) ValueError異常;若參數(shù)是列表,因為類型不正確,所以引發(fā)一個TypeError異常。
def safe_float(obj):
try:
retval = float(obj)
except ValueError:
retval = 'could not convert non-number to float'
return retval
(3)帶有多個except 的try語句
把多個except語句連接在一起, 處理一個try塊中可能發(fā)生的多種異常。
except Exception1[, reason1]:
suite_for_exception_Exception1
except Exception2[, reason2]:
suite_for_exception_Exception2
程序首先嘗試執(zhí)行try子句,如果沒有錯誤,忽略所有的except從句繼續(xù)執(zhí)行。如果發(fā)生異常,解釋器將在這一串處理器(except 子句)中查找匹配的異常, 如果找到對應(yīng)的處理器,執(zhí)行流將跳轉(zhuǎn)到這里。
Python支持把except語句串連使用,分別為每個異常類型分別創(chuàng)建對應(yīng)的錯誤信息,這樣用戶可以得到更詳細的關(guān)于錯誤的信息。
def safe_float(obj):
try:
retval = float(obj)
except ValueError:
retval = 'could not convert non-number to float'
except TypeError:
retval = 'object type cannot be converted to float'
return retval
(4)處理多個異常的except語句
except 語句可以處理任意多個異常,但要求異常被放在一個元組里。
except (Exc1[, Exc2[, ... ExcN]])[, reason]:
suite_for_exceptions_Exc1_to_ExcN
如:
def safe_float(obj):
try:
retval = float(obj)
except (ValueError, TypeError):
retval = 'argument must be a number or numeric string'
return retval
(5)捕獲所有異常
捕獲所有的異常的代碼:
try:
pass
except Exception, e:
# error occurred, log 'e', etc.
或不太推薦的方法是使用裸except子句(無任何錯誤信息,以后可能會不再支持):
try:
pass
except:
# error occurred, etc.
異常部分內(nèi)容在Python 2.5有了一些變化,異常被遷移到了new-style class上,啟用了一個新的"所有異常的母親",這個類叫做 BaseException。異常的繼承結(jié)構(gòu)有了少許調(diào)整,KeyboardInterrupt和SystemExit被從Exception里移出,和Exception平級。
- BaseException
|- KeyboardInterrupt
|- SystemExit
|- Exception
|- (all other current built-in exceptions) 所有當(dāng)前內(nèi)建異常
若需要捕獲所有異常,那么就可以使用新的BaseException:
try:
pass
except BaseException, e:
# handle all errors
注意:避免把大片的代碼裝入try-except中然后使用pass忽略掉錯誤,可以捕獲特定的異常并忽略它們,或是捕獲所有異常并采取特定的動作.,不要捕獲所有異常,然后忽略掉它們。
(6)異常參數(shù)
異常也可以有參數(shù),異常引發(fā)后它會被傳遞給異常處理器。當(dāng)異常被引發(fā)后參數(shù)是作為附加幫助信息傳遞給異常處理器的。雖然異常原因是可選的,但標準內(nèi)建異常提供至少一個參數(shù),指示異常原因的一個字符串。異常的參數(shù)可以在處理器里忽略, 但 Python提供了保存這個值的語法。要想訪問提供的異常原因,必須保留一個變量來保存這個參數(shù),把這個參數(shù)放在except語句后,接在要處理的異常后面。except 語句的這個語法可以被擴展為:
# single exception
except Exception[, reason]:
suite_for_Exception_with_Argument
# multiple exceptions
except (Exception1, Exception2, ..., ExceptionN)[, reason]:
suite_for_Exception1_to_ExceptionN_with_Argument
其中reason將會是一個包含來自導(dǎo)致異常的代碼的診斷信息的類實例。異常參數(shù)自身會組成一個元組,并存儲為類實例(異常類的實例)的屬性,reason將會是一個Exception類的實例。
無論reason只包含一個字符串或是由錯誤編號和字符串組成的元組,調(diào)用 str(reason) 總會返回一個良好可讀的錯誤原因,因為 reason是一個類實例,這樣其實是調(diào)用類的特殊方法str() 。
唯一的問題就是某些第三方或是其他外部庫并不遵循標準協(xié)議, 推薦在引發(fā)自己的異常時遵循異常參數(shù)規(guī)范,用和已有Python代碼一致錯誤信息作為傳給異常的參數(shù)元組的一部分。如:若引發(fā)一個ValueError,那么最好提供和解釋器引發(fā)ValueError時一致的參數(shù)信息。
(7)在應(yīng)用使用我們封裝的函數(shù)
處理一個文件,將其作為字符串讀入,并用一個日志文件跟蹤處理進程。
#!/usr/bin/env python
def safe_float(object):
'safe version of float()'
try:
retval = float(object)
except (TypeError, ValueError), diag:
retval = str(diag)
return retval
def main():
'handles all the data processing'
log = open('cardlog.txt', 'w')
try:
ccfile = open('carddata.txt', 'r')
except IOError, e:
log.write('no txns this month\n')
log.close()
return
txns = ccfile.readlines()
ccfile.close()
total = 0.00
log.write('account log:\n')
for eachTxn in txns:
result = safe_float(eachTxn)
if isinstance(result, float):
total += result
log.write('data... processed\n')
else:
log.write('ignored: %s' % result)
print '$%.2f (new balance)' % (total)
log.close()
if __name__ == '__main__':
main()
其中從文件中提取數(shù)據(jù),這里的文件打開被置于try-except語句段中。內(nèi)建的isinstance()函數(shù)檢查結(jié)果類型,檢查safe_float是返回字符串還是浮點數(shù),任何字符串都意味著錯誤,表明該行不能轉(zhuǎn)換為數(shù)字,同時所有的其他數(shù)字可以作為浮點數(shù)累加入total,在 main()函數(shù)的尾行會顯示最終生成的余額。
(8)else子句
在try范圍中沒有異常被檢測到時即結(jié)束前沒有引發(fā)異常,然后執(zhí)行else子句。如:
try:
function()
except:
pass
else:
pass
(9)finally子句
finally子句是無論異常是否發(fā)生、是否捕捉都會執(zhí)行的一段代碼。可以將finally僅僅配合try一起使用,也可以和 try-except(else也是可選的)一起使用。
try-except-else-finally 語法的示例:
try:
A
except MyException:
B
else:
C
finally:
D
(10)try-finally語句
另一種使用finally的方式是finally單獨和try連用。這個try-finally語句和try-except區(qū)別在于它不是用來捕捉異常的。作為替代,它常常用來維持一致的行為,無論 try中是否有異常觸發(fā),finally 代碼段都會被執(zhí)行。
try:
try_suite
finally: #無論如何都執(zhí)行
finally_suite
當(dāng)在try范圍中產(chǎn)生一個異常時,會立即跳轉(zhuǎn)到finally語句段。當(dāng)finally中的所有代碼都執(zhí)行完畢后,會繼續(xù)向上一層引發(fā)異常。
讀取數(shù)據(jù)的代碼:
try:
ccfile = open('carddata.txt', 'r')
txns = ccfile.readlines()
ccfile.close()
except IOError:
log.write('no txns this month\n')
其中的缺陷:若按照這樣的順序發(fā)生錯誤,打開成功但出于一些原因readlines()調(diào)用失敗,異常處理會去繼續(xù)執(zhí)行except中的子句,而不去嘗試關(guān)閉文件。通過try-finally來優(yōu)化:
ccfile = None
try:
try:
ccfile = open('carddata.txt', 'r')
txns = ccfile.readlines()
except IOError:
log.write('no txns this month\n')
finally:
if ccfile:
ccfile.close()
另一種可選的實現(xiàn)切換了try-except和try-finally包含的方式:
ccfile = None
try:
try:
ccfile = open('carddata.txt', 'r')
txns = ccfile.readlines()
finally:
if ccfile:
ccfile.close()
except IOError:
log.write('no txns this month\n')
上述方法唯一的問題是:當(dāng)finally中的代碼引發(fā)了另一個異常或由于return、break、continue語法而終止,會丟失原來異常的上下文信息導(dǎo)致原來的異常無法重新引發(fā),除非也事先保存。
(11)try-except-else-finally:廚房一鍋端
語法樣式:
try:
try_suite
except Exception1:
suite_for_Exception1
except (Exception2, Exception3, Exception4):
suite_for_Exceptions_2_3_and_4
except Exception5, Argument5:
suite_for_Exception5_plus_argument
except (Exception6, Exception7), Argument67:
suite_for_Exceptions6_and_7_plus_argument
except:
suite_for_all_other_exceptions
else:
no_exceptions_detected_suite
finally:
always_execute_suite
其中else和finally都是可選的,而必須至少要有一個except子句。
4、上下文管理
(1)with 語句
另一個隱藏低層次的抽象的例子是with語句,它在Python 2.6中正式啟用(以前必必須用from future import with_statement來導(dǎo)入它)。
with 語句的目的在于從流程圖中把try、except和finally關(guān)鍵字和資源分配釋放相關(guān)代碼統(tǒng)統(tǒng)去掉,而不是像try-except-finally那樣僅僅簡化代碼使之易用。 with 語法的基本用法:
with context_expr [as var]:
with_suite
file和with一起使用的代碼片段:
with open('/etc/passwd', 'r') as f:
for eachLine in f:
# ...do stuff with eachLine or f...
這段代碼試圖打開一個文件,如果一切正常,把文件對象賦值給f;然后用迭代器遍歷文件中的每一行,當(dāng)完成時關(guān)閉文件。無論在這一段代碼的開始、中間還是結(jié)束時發(fā)生異常,都會執(zhí)行清理的代碼,此外文件仍會被自動的關(guān)閉。可以看出Python已經(jīng)拿走了一堆細節(jié),實際上只是進行了兩層處理:第一,用戶層 ,和in類似所需要關(guān)心的只是被使用的對象;第二,對象層,既然這個對象支持上下文管理協(xié)議,它干的也就是"上下文管理"。
5、字符串作為異常
早在Python 1.5前,標準的異常是基于字符串實現(xiàn)的。然而,這樣就限制了異常之間不能有相互的關(guān)系,這種情況隨著異常類的來臨而不復(fù)存在。到1.5后,所有的標準異常都是類了,但程序員還是可以用字符串作為自己的異常的(建議使用異常類)。在2.5中,觸發(fā)字符串異常會導(dǎo)致一個警告,2.6中捕獲字符串異常會導(dǎo)致一個警告。
6、觸發(fā)異常
有些異常由程序執(zhí)行期間的錯誤而引發(fā),程序員在編寫API時希望遇到錯誤的輸入時觸發(fā)異常,為此Python提供了一種機制讓程序員明確的觸發(fā)異常,即raise語句。
(1)raise語法
raise 語句支持的參數(shù)十分靈活,語法上支持許多不同的格式。rasie一般的用法:raise [SomeException [, args [, traceback]]]。
第一個參數(shù),SomeExcpetion是觸發(fā)異常的名字。如果有,它必須是一個字符串、類或?qū)嵗H绻衅渌麉?shù)(arg或traceback),就必須提供SomeExcpetion.Python的標準異常。
第二個符號為可選的args(比如參數(shù),值)來傳給異常,這可以是一個單獨的對象也可以是一個對象的元組。當(dāng)異常發(fā)生時,異常的參數(shù)總是作為一個元組傳入。如果args原本就是元組,那么就將其傳給異常去處理;如果args是一個單獨的對象,就生成只有一個元素的元組(就是單元素元組)。大多數(shù)情況下,單一的字符串用來指示錯誤的原因。如果傳的是元組,通常的組成是一個錯誤字符串,一個錯誤編號,可能還有一個錯誤的地址比如文件等等。
最后一項參數(shù)traceback,同樣是可選的(實際上很少用它)。如果有的話,則是當(dāng)異常觸發(fā)時新生成的一個用于異常-正常化(exception-normally)的追蹤(traceback)對象。當(dāng)你想重新引發(fā)異常時,第三個參數(shù)很有用(可以用來區(qū)分先前和當(dāng)前的位置)。如果沒有這個參數(shù),就填寫None。
(2) raise慣用法
最常見的用法為SomeException是一個類,不需要其他的參數(shù),但如果有的話,可以是一個單一對象參數(shù)、一個參數(shù)的元組或一個異常類的實例。如果參數(shù)是一個實例,可以由給出的類及其派生類實例化(已存在異常類的子集)。若參數(shù)為實例,則不能有更多的其他參數(shù)。
(3)raise少見的慣用法
當(dāng)參數(shù)是一個實例 , 該實例若是給定異常類的實例當(dāng)然不會有問題,然而如果該實例并非這個異常類或其子類的實例時,那么解釋器將使用該實例的異常參數(shù)創(chuàng)建一個給定異常類的新實例。 如果該實例是給定異常類子類的實例, 那么新實例將作為異常類的子類出現(xiàn), 而不是原來的給定異常類。
如果raise語句的額外參數(shù)不是一個實例——作為替代,是一個單件(singleton)或元組,那么將用這些作為此異常類的初始化的參數(shù)列表。如果不存在第二個參數(shù)或是None,則參數(shù)列表為空。
如果SomeException是一個實例,就無需對什么進行實例化了。這種情況下,不能有額外的參數(shù)或只能是None。異常的類型就是實例的類;也就是說,等價于觸發(fā)此類異常,并用該實例為參數(shù)比如:raiseinstance.class,instance。
還有一個可選的參量(args)作參數(shù).
最后,這種不含任何參數(shù)的raise語句結(jié)構(gòu)是在Python1.5中新引進的,會引發(fā)當(dāng)前代碼塊(code block)最近觸發(fā)的一個異常。如果之前沒有異常觸發(fā),會因為沒有可觸發(fā)的異常而生成一TypeError異常。
(4)raise的不同用法
由于raise有許多不同格式有效語法(比如:SomeException 可以是類、實例或一個字符串),以下總結(jié)下rasie 的不同用法:
7、斷言
斷言是一句必須等價于布爾真的判定;此外,發(fā)生異常也意味著表達式為假(類似C中預(yù)處理器的assert宏),但在Python中它們在運行時構(gòu)建(與之相對的是編譯期判別)。 斷言通過 assert 語句實現(xiàn)。可以簡簡單單的想象為raise-if語句(更準確的說是raise-if-not 語句)。測試一個表達式,若返回值是假,觸發(fā)異常。
斷言語句,如果斷言成功不采取任何措施(類似語句),否則觸發(fā)AssertionError(斷言錯誤)的異常。assert 的語法如下:assert expression[, arguments]。AssertionError異常和其他的異常一樣可以用try-except語句塊捕捉,但是如果沒有捕捉,它將終止程序運行而且提供一個traceback。
如同raise 語句一樣,也可以提供一個異常參數(shù)給assert 命令:
>>> try:
... assert 1 == 0, 'One does not equal zero silly!'
... except AssertionError, args:
... print '%s: %s' % (args.__class__.__name__, args)
...
AssertionError: One does not equal zero silly!
assert 如何運作,可以通過函數(shù)類似實現(xiàn)(內(nèi)建的變量debug在通常情況下為True):
def assert(expr, args=None):
if __debug__ and not expr:
raise AssertionError, args
8、標準異常
所有的異常都是內(nèi)建的, 所以它們在腳本啟動前或在互交命令行提示符出現(xiàn)時已經(jīng)是可用的了。
Python內(nèi)建異常:
所有的標準/內(nèi)建異常都是從根異常派生的。目前有3個直接從BaseException派生的異常子類:SystemExit、KeyboardInterrupt和Exception,其他的所有的內(nèi)建異常都是Exception的子類。
到了Python2.5,所有的異常的都是新風(fēng)格(new-style)的類,并且最終都是BaseException的子類。從Python1.5到Python2.4.x,異常是標準的類,它們是字符串。從Python2.5開始,不再支持構(gòu)建基于字符串的異常并且被正式的棄用,即不能再觸發(fā)一個字符串異常了,將不能捕獲它們。還有一個就是所有新的異常最終都是BaseException的子類,以便于它們有一個統(tǒng)一的接口。
9、sys和相關(guān)模塊
(1)sys模塊
另一種獲取異常信息的途徑是通過sys模塊中exc_info()函數(shù),此功能提供了一個3元組(3-tuple)的信息。
>>> try:
... float('abc123')
... except:
... import sys
... exc_tuple = sys.exc_info()
...
>>> print exc_tuple
(<type 'exceptions.ValueError'>, ValueError('invalid literal for float(): abc123',), <traceback object at 0x7ff79c87dc20>)
>>> for eachItem in exc_tuple:
... print eachItem
...
<type 'exceptions.ValueError'>
invalid literal for float(): abc123
<traceback object at 0x7ff79c87dc20>
sys.exc_info()得到的元組中是:exc_type異常類;exc_value異常類的實例;exc_traceback追蹤(traceback)對象。第三項,,是一個新增的追蹤(traceback)對象,這一對象提供了的發(fā)生異常的上下文,它包含諸如代碼的執(zhí)行幀,異常發(fā)生時的行號等信息。在舊版本中的Python中,這三個值分別存在于sys模塊,為sys.exc_type、sys.exc_value和sys.exc_traceback ,但這三者是全局變量而不是線程安全的, 建議用sys.exc_info()來代替。
(2)相關(guān)模塊
模塊 描述
exceptions 內(nèi)建異常(永遠不用導(dǎo)入這個模塊)
contextlib 為使用 with 語句的上下文對象工具
sys 包含各種異常相關(guān)的對象和函數(shù)(見sys.ex)
三、總結(jié)
(1)Python異常不僅簡化代碼,而且簡化整個錯誤管理體系,異常處理促使成熟和正確的編程。
(2)可以通過創(chuàng)建一個從內(nèi)置的Exception類繼承的類定義自己的異常,然后使用raise命令引發(fā)異常或傳遞異常。