程式員的一生中,錯誤幾乎每天都在發生。在過去的一個時期, 錯誤要麼對程式(可能還有機器)是致命的,要麼產生一大堆無意義的輸出,無法被其他電腦或程式識別,連程式員自己也可能搞不懂它的意義。一旦出現錯誤,程式就會終止執行,直到錯誤被修正,程式重新執行。所以,人們需要一個”柔和”的處理錯誤的方法,而不是終止程式。同時,程式本身也在不斷髮展,並不是每個錯誤都是致命的,即使錯誤發生,編譯器或是在執行中的程式也可以提供更多更有用的診斷資訊,協助程式員儘快解決問題。然而,錯誤畢竟是錯誤,一般都是停止編譯或執行後才能去解決它。一小段代碼只能讓程式終止執行,也許還能列印出一些模糊的提示。當然,這一切都是在異常和異常處理出現之前的事了。
1. 錯誤
從軟體方面來說,錯誤是文法或是邏輯上的。語法錯誤指示軟體的結構上有錯誤,導致不能被解譯器解釋或編譯器無法編譯。這些錯誤必須在程式執行前糾正。當程式的文法正確後,剩下的就是邏輯錯誤了。邏輯錯誤可能是由於不完整或是不合法的輸入所致;在其他情況下,還可能是邏輯無法產生,計算,或是輸出結果需要的過程無法執行。這些錯誤通常分別被稱為域錯誤和範圍錯誤。
當 Python 檢測到一個錯誤時,解譯器就會指出當前流已經無法繼續執行下去,這時候就出現了異常。
2. 異常
對異常的最好描述是: 它是因為程式出現了錯誤而在正常控制流程以外採取的行為。這個行為又分為兩個階段: 首先是引起異常發生的錯誤,然後是檢測(和採取可能的措施)階段。
第一個階段是在發生了一個異常條件(有時候也叫做例外的條件)後發生的。只要檢測到錯誤並且意識到異常條件,解譯器會引發一個異常。引發也可以叫做觸發或者產生,解譯器通過它通知當前控制流程有錯誤發生。Python 也允許程式員自己引發異常,無論是 Python 解譯器還是程式員引發的,異常就是錯誤發生的訊號,當前流將被打斷,用來處理這個錯誤並採取相應的操作,這就是第二階段。
對異常的處理髮生在第二階段。異常引發後,可以調用很多不同的操作,可以是忽略錯誤(記錄錯誤但不採取任何措施, 採取補救措施後終止程式),或是減輕問題的影響後設法繼續執行程式。所有的這些操作都代表一種繼續,或是控制的分支,關鍵是程式員在錯誤發生時可以指示程式如何執行。
類似 Python 這樣支援引發和處理異常(這更重要)的語言,可以讓開發人員可以在錯誤發生時更直接地控制它們。程式員不僅僅有了檢測錯誤的能力,還可以在它們發生時採取更可靠的補救措施。由於有了運行時管理錯誤的能力,應用程式的健壯性有了很大的提高。
異常和異常處理並不是什麼新概念。它們同樣存在於 Ada,Modula-3,C++,Eiffel,以及 Java 中。異常的起源可以追溯到處理系統錯誤和硬體中斷這類異常的作業系統代碼。在 1965 年左右,PL/1 作為第一個支援異常的主要語言出現,而異常處理是作為一個它提供的軟體工具。和其他支援異常處理的語言類似,Python 採用了 “try/嘗試” 塊和 “catching/捕獲” 塊的概念,而且它在異常處理方面更有”紀律性”。我們可以為不同的異常建立不同的處理器,而不是盲目地建立一個”catch-all/捕獲所有”的代碼。
3. python中常見異常
NameError:嘗試訪問一個未聲明的變數
>>> hpTraceback (most recent call last): File "<stdin>", line 1, in <module>NameError: name 'hp' is not defined>>>
NameError 表示我們訪問了一個沒有初始化的變數. 在 Python 解譯器的符號表沒有找到那個另人討厭的變數. 我們將在後面的兩章討論名稱空間, 現在大家可以認為它們是串連名字和對象的”地址簿”就可以了. 任何可訪問的變數必須在名稱空間裡列出. 訪問變數需要由解譯器進行搜尋, 如果請求的名字沒有在任何名稱空間裡找到, 那麼將會產生一個 NameError 異常.
ZeroDivisionError:除數為零
>>> 1/0Traceback (most recent call last): File "<stdin>", line 1, in <module>ZeroDivisionError: integer division or modulo by zero>>>
我們邊的例子使用的是整數, 但事實上, 任何數值被零除都會導致一個 ZeroDivisionError 異常.
SyntaxError:Python 解譯器語法錯誤
>>> if File "<stdin>", line 1 if ^SyntaxError: invalid syntax
SyntaxError 異常是唯一不是在運行時發生的異常. 它代表 Python 代碼中有一個不正確的結構, 在它改正之前程式無法執行. 這些錯誤一般都是在編譯時間發生, Python 解譯器無法把你的指令碼轉化為 Python 位元組代碼. 當然這也可能是你匯入一個有缺陷的模組的時候.
IndexError:請求的索引超出序列範圍
>>> aList = ['hp']>>> aList[1]Traceback (most recent call last): File "<stdin>", line 1, in <module>IndexError: list index out of range
IndexError 在你嘗試使用一個超出範圍的值索引序列時引發.
KeyError:請求一個不存在的字典關鍵字
>>> aDict = {'hp':'pavilion','num':8888}>>> print aDict['HP']Traceback (most recent call last): File "<stdin>", line 1, in <module>KeyError: 'HP'
映射對象, 例如字典, 是依靠關鍵字(keys)訪問資料值的. 如果使用錯誤的或是不存在的鍵請求字典就會引發一個 KeyError 異常.
IOError:輸入/輸出錯誤
>>> f = open("hp")Traceback (most recent call last): File "<stdin>", line 1, in <module>IOError: [Errno 2] No such file or directory: 'hp'
類似嘗試開啟一個不存在的磁碟檔案一類的操作會引發一個作業系統輸入/輸出(I/O)錯誤. 任何類型的 I/O 錯誤都會引發 IOError 異常.
AttributeError:嘗試訪問未知的對象屬性
>>> class myClass(object):... pass... >>> myInst = myClass()>>> myInst.hp = 'pavilion'>>> myInst.hp'pavilion'>>> myInst.hqTraceback (most recent call last): File "<stdin>", line 1, in <module>AttributeError: 'myClass' object has no attribute 'hq'
我們在 myInst.hp 儲存了一個值, 也就是執行個體 myInst 的 hp 屬性. 屬性被定義後, 我們可以使用熟悉的點/屬性操作符訪問它, 但如果是沒有定義屬性, 例如我們訪問 hq 屬性, 將導致一個 AttributeError 異常.
4. 檢測和處理異常
異常可以通過 try 語句來檢測。任何在 try 語句塊裡的代碼都會被監測,檢查有無異常發生。
try 語句有兩種主要形式: try-except 和 try-finally . 這兩個語句是互斥的, 也就是說你只 能 使 用 其 中 的 一 種 . 一 個 try 語 句 可 以 對 應 一 個 或 多 個 except 子 句 , 但 只 能 對 應 一 個 finally 子句, 或是一個 try-except-finally 複合陳述式.
你可以使用 try-except 語句檢測和處理異常. 你也可以添加一個可選的 else 子句處理沒有探測到異常的時執行的代碼. 而 try-finally 只允許檢測異常並做一些必要的清除工作(無論發生錯誤與否), 沒有任何異常處理設施. 正如你想像的, 複合陳述式兩者都可以做到.
try-except 語句
最 常 見 的 try-except 語 句 語 法 如 下 所 示,它 由 try 塊 和 except 塊 (try_suite 和 except_suite )組成,也可以有一個可選的錯誤原因。
try: try_suite #watch for exceptions here 監控這裡的異常except Exception[, reason]: except_suite #exception-handling code 異常處理代碼
>>> try:... f = open('hp', 'r')... except IOError, e:... print 'could not open file:', e... could not open file: [Errno 2] No such file or directory: 'hp'
帶有多個 except 的 try 語句
這種格式的 except 語句指定檢測名為 Exception 的異常. 你可以把多個 except 語句串連在一起, 處理一個 try 塊中可能發生的多種異常, 如下所示:
try: try_suiteexcept Exception1[, reason1]: suite_for_exception_Exception1except Exception2[, reason2]: suite_for_exception_Exception2
>>> def safe_float(obj):... try:... retval = float(obj)... except ValueError:... retval = 'could not convert non-number to float'... except TypeError:... retval = 'object type can not be converted to float'... return retval... >>> safe_float('hp')'could not convert non-number to float'>>> safe_float({'hp':'pavilion'})'object type can not be converted to float'>>> safe_float('200')200.0>>> safe_float(200)200.0
處理多個異常的 except 語句
我們還可以在一個 except 子句裡處理多個異常,前提只是它們被放入一個元組裡 , 如下所示:
try: try_suiteexcept (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 numberic string'... return retval... >>> safe_float([])'argument must be a number or numberic string'>>> safe_float("hp")'argument must be a number or numberic string'>>> safe_float("123")123.0>>> safe_float(123)123.0
Note: try 語句塊中異常發生點後的剩餘語句永遠不會到達(所以也永遠不會執行)。一旦一個異常被引發,就必須決定控制流程下一步到達的位置。剩餘代碼將被忽略,解譯器將搜尋處理器,一旦找到,就開始執行處理器中的代碼。
如果沒有找到合適的處理器,那麼異常就向上移交給調用者去處理,這意味著堆棧架構立即回到之前的那個。如果在上層調用者也沒找到對應處理器,該異常會繼續被向上移交,直到找到合適處理器。如果到達最頂層仍然沒有找到對應處理器,那麼就認為這個異常是未處理的,Python 解譯器會顯示出跟蹤返回訊息,然後退出。
Python 提供給程式員的 try-except 語句是為了更好地跟蹤潛在的錯誤並在代碼裡準備好處理異常的邏輯,這樣的機制在其他語言(例如 C ) 是很難實現的,它的目的是減少程式出錯的次數並在出錯後仍能保證程式正常執行。作為一種工具而言,只有正確得當地使用它,才能使其發揮作用。
避免把大片的代碼裝入 try-except 中然後使用 pass 忽略掉錯誤,你可以捕獲特定的異常並忽略它們,或是捕獲所有異常並採取特定的動作。不要捕獲所有異常,然後忽略掉它們。
異常參數
異常也可以有參數,異常引發後它會被傳遞給異常處理器。當異常被引發後參數是作為附加協助資訊傳遞給異常處理器的。雖然異常原因是可選的,但標準內建異常提供至少一個參數,指示異常原因的一個字串。
異常的參數可以在處理器裡忽略,但 Python 提供了儲存這個值的文法,我們已經在上邊接觸到相關內容:要想訪問提供的異常原因,你必須保留一個變數來儲存這個參數。把這個參數放在 except 語句後,接在要處理的異常後面。
except 語句的這個文法可以被擴充為:# single exceptionexcept Exception[, reason]: suite_for_Exception_with_Argument# multiple exceptionsexcept (Exception1, Exception2, ..., ExceptionN)[, reason]: suite_for_Exception1_to_ExceptionN_with_Argument
reason 將會是一個包含來自導致異常的代碼的診斷資訊的類執行個體。異常參數自身會組成一個元組,並儲存為類執行個體 ( 異 常 類 的 實 例 ) 的 屬 性 。上 邊 的 第 一 種 用 法 中,reason 將 會 是 一 個 Exception 類的執行個體。
>>> def safe_float(object):... try:... retval = float(object)... except (ValueError, TypeError), diag:... retval = str(diag)... return retval... >>> safe_float('hp')'could not convert string to float: hp'>>> safe_float({})'float() argument must be a string or a number'
else 子句
我們已經看過 else 語句段配合其他的 Python 語句,比如條件和迴圈。至於 try-except 語句段,它的功能和你所見過的其他 else 沒有太多的不同:在 try 範圍中沒有異常被檢測到時,執行 else 子句。
在 else 範圍中的任何代碼運行前,try 範圍中的所有代碼必須完全成功(也就是,結束前沒有引發異常)。
try: try_suiteexcept Exception[, reason]: suite_for_exception_Exceptionelse: else_suite # 無異常時執行此語句
>>> def safe_float(object):... try:... float(object)... except (ValueError, TypeError), diag:... retval = str(diag)... else:... retval = float(object)... return retval... >>> safe_float("hp")'could not convert string to float: hp'>>> safe_float({})'float() argument must be a string or a number'>>> safe_float("123")123.0>>> safe_float(123)123.0
finally 子句
finally 子句是無論異常是否發生,是否捕捉都會執行的一段代碼。你可以將 finally 僅僅配合 try 一起使用,也可以和 try-except(else 也是可選的) 一起使用,也可以使用獨立的 try-finally。
try: Aexcept MyException: Belse: Cfinally: D
當然,無論如何,你都可以有不止一個的 except 子句,但最少有一個 except 語句,而 else 和 finally 都是可選的。A,B,C 和 D 是程式(代碼塊)。程式會按預期的順序執行。(注意:可能的順序是A-C-D[正常] 或 A-B-D[異常])。無論異常發生在 A,B,和/或 C 都將執行 finally 塊。
另一種使用 finally 的方式是 finally 單獨和 try 連用。這個 try-finally 語句和 try-except 區別在於它不是用來捕捉異常的。作為替代,它常常用來維持一致的行為而無論異常是否發生。我們得知無論 try 中是否有異常觸發,finally 程式碼片段都會被執行。
try: try_suitefinally: finally_suite #無論如何都執行
try-except-else-finally語句
try: try_suiteexcept Exception1: suite_for_Exception1except (Exception2, Exception3, Exception4): suite_for_Exceptions_2_3_and_4except Exception5, Argument5: suite_for_Exception5_plus_argumentexcept (Exception6, Exception7), Argument67: suite_for_Exceptions6_and_7_plus_argumentexcept: suite_for_all_other_exceptionselse: no_exceptions_detected_suitefinally: always_execute_suite
無論你選擇什麼文法,你至少要有一個 except 子句,而 else 和 finally 都是可選的。
with語句
with 語句的目的在於從流程圖中把 try,except 和 finally 關鍵字和資源分派釋放相關代碼統統去掉,而不是像 try-except-finally 那樣僅僅簡化代碼使之易用。with 文法的基本用法看上去如下:
with context_expr [as var]: with_suite
with open('/etc/passwd', 'r') as f: for eachLine in f: # ...do stuff with eachLine or f...
這段代碼試圖開啟一個檔案,如果一切正常,把檔案對象賦值給 f。然後,用迭代器遍曆檔案中的每一行,當完成時,關閉檔案。無論在這一段代碼的開始,中間,還是結束時發生異常,都會執行清理的代碼,此外檔案仍會被自動的關閉。
5. 觸發異常
raise語句
raise 語句對所支援是參數十分靈活,對應到文法上就是支援許多不同的格式.rasie 一般的用法是:
raise [SomeException [, args [, traceback]]]
第一個參數,SomeExcpetion,是觸發異常的名字.如果有,它必須是一個字串,類或執行個體(詳見下文).如果有其他參數(arg 或 traceback),就必須提供 SomeExcpetion.
第二個符號為可選的 args(比如參數,值),來傳給異常.這可以是一個單獨的對象也可以是一個對象的元組.當異常發生時,異常的參數總是作為一個元組傳入.如果 args 原本就是元組,那麼就將其傳給異常去處理;如果 args 是一個單獨的對象,就產生只有一個元素的元組(就是單元素元組).大多數情況下,單一的字串用來指示錯誤的原因.如果傳的是元組,通常的組成是一個錯誤字串,一個錯誤編號,可能還有一個錯誤的地址,比如檔案,等等.
最後一項參數,traceback,同樣是可選的(實際上很少用它),如果有的話,則是當異常觸發時新產生的一個用於異常-正常化(exception—normally)的追蹤(traceback)對象.當你想重新引發異常時,第三個參數很有用(可以用來區分先前和當前的位置).如果沒有這個參數,就填寫 None.
6. Assert 陳述式
Assert 陳述式等價於這樣的 Python 運算式,如果斷言成功不採取任何措施(類似語句),否則觸發AssertionError(斷言錯誤)的異常.assert 的文法如下:
assert expression[, arguments]
>>> assert 1==1>>> assert 1 == 1>>> assert 2 + 2 == 2 * 2>>> assert len(['hp', '5']) < 10>>> assert range(3) == [0, 1, 2]>>> assert 1 == 0Traceback (most recent call last): File "<stdin>", line 1, in <module>AssertionError
>>> 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!