3.4 異常
譯者:飛龍
程序員必須總是留意程序中可能出現的錯誤。例子數不勝數:一個函數可能不會收到它預期的信息,必需的資源可能會丟失,或者網絡上的連接可能丟失。在設計系統時,程序員必須預料到可能產生的異常情況並且採取適當地措施來處理它們。
處理程序中的錯誤沒有單一的正確方式。為提供一些持久性服務而設計的程序,例如 Web 服務器 應該對錯誤健壯,將它們記錄到日誌中為之後考慮,而且在儘可能長的時間內繼續接受新的請求。另一方面,Python 解釋器通過立即終止以及打印錯誤信息來處理錯誤,便於程序員在錯誤發生時處理它。在任何情況下,程序員必須決定程序如何對異常條件做出反應。
異常是這一節的話題,它為程序的錯誤處理提供了通用的機制。產生異常是一種技巧,終止程序正常執行流,發射異常情況產生的信號,並直接返回到用於響應異常情況的程序的封閉部分。Python 解釋器每次在檢測到語句或表達式錯誤時拋出異常。用戶也可以使用raise或assert語句來拋出異常。
**拋出異常。**異常是一個對象實例,它的類直接或間接繼承自BaseException類。第一章引入的assert語句產生AssertionError類的異常。通常,異常實例可以使用raise語句來拋出。raise語句的通用形式在 Python 文檔中描述。raise的最常見的作用是構造異常實例並拋出它。
>>> raise Exception('An error occurred')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: an error occurred
當異常產生時,當前代碼塊的語句不會繼續執行。除非異常被解決了(下面會描述),解釋器會直接返回到“讀取-求值-打印”交互式循環中,或者在 Python 以文件參數啟動的情況下會完全終止。此外,解釋器會打印棧回溯,它是結構化的文本塊,描述了執行分支中的一系列嵌套的活動函數,它們是異常產生的位置。在上面的例子中,文件名稱<stdin>表示異常由用戶在交互式會話中產生,而不是文件中的代碼。
**處理異常。**異常可以使用封閉的try語句來處理。try語句由多個子句組成,第一個子句以try開始,剩下的以except開始。
try:
<try suite>
except <exception class> as <name>:
<except suite>
...
當try語句執行時,<try suite>總是會立即執行。except子句組只在<try suite>執行過程中的異常產生時執行。每個except子句指定了需要處理的異常的特定類。例如,如果<exception class>是AssertionError,那麼任何繼承自AssertionError的類實例都會被處理,標識符<name> 綁定到所產生的異常對象上,但是這個綁定在<except suite>之外並不有效。
例如,我們可以使用try語句來處理異常,在異常發生時將x綁定為0。
>>> try:
x = 1/0
except ZeroDivisionError as e:
print('handling a', type(e))
x = 0
handling a <class 'ZeroDivisionError'>
>>> x
0
try語句能夠處理產生在函數體中的異常,函數在<try suite>中調用。當異常產生時,控制流會直接跳到最近的try語句的能夠處理該異常類型的<except suite>的主體中。
>>> def invert(x):
result = 1/x # Raises a ZeroDivisionError if x is 0
print('Never printed if x is 0')
return result
>>> def invert_safe(x):
try:
return invert(x)
except ZeroDivisionError as e:
return str(e)
>>> invert_safe(2)
Never printed if x is 0
0.5
>>> invert_safe(0)
'division by zero'
這個例子表明,invert中的print表達式永遠不會求值,反之,控制流跳到了handler中的except子句組中。將ZeroDivisionError e強制轉為字符串會得到由handler: 'division by zero'返回的人類可讀的字符串。
3.4.1 異常對象
異常對象本身就帶有屬性,例如在assert語句中的錯誤信息,以及有關異常產生處的信息。用戶定義的異常類可以攜帶額外的屬性。
在第一章中,我們實現了牛頓法來尋找任何函數的零點。下面的例子定義了一個異常類,無論何時ValueError出現,它都返回迭代改進過程中所發現的最佳猜測值。數學錯誤(ValueError的一種)在sqrt在負數上調用時產生。這個異常由拋出IterImproveError處理,它將牛頓迭代法的最新猜測值儲存為參數。
首先,我們定義了新的類,繼承自Exception。
>>> class IterImproveError(Exception):
def __init__(self, last_guess):
self.last_guess = last_guess
下面,我們定義了IterImprove,我們的通用迭代改進算法的一個版本。這個版本通過拋出IterImproveError異常,儲存最新的猜測值來處理任何ValueError。像之前一樣,iter_improve接受兩個函數作為參數,每個函數都接受單一的數值參數。update函數返回新的猜測值,而done函數返回布爾值,表明改進是否收斂到了正確的值。
>>> def iter_improve(update, done, guess=1, max_updates=1000):
k = 0
try:
while not done(guess) and k < max_updates:
guess = update(guess)
k = k + 1
return guess
except ValueError:
raise IterImproveError(guess)
最後,我們定義了find_root,它返回iter_improve的結果。iter_improve應用於由newton_update返回的牛頓更新函數。newton_update定義在第一章,在這個例子中無需任何改變。find_root的這個版本通過返回它的最後一個猜測之來處理IterImproveError。
>>> def find_root(f, guess=1):
def done(x):
return f(x) == 0
try:
return iter_improve(newton_update(f), done, guess)
except IterImproveError as e:
return e.last_guess
考慮使用find_root來尋找2 * x ** 2 + sqrt(x)的零點。這個函數的一個零點是0,但是在任何負數上求解它會產生ValueError。我們第一章的牛頓法實現會產生異常,並且不能返回任何零點的猜測值。我們的修訂版實現在錯誤之前返回了最新的猜測值。
>>> from math import sqrt
>>> find_root(lambda x: 2*x*x + sqrt(x))
-0.030211203830201594
雖然這個近似值仍舊距離正確的答案0很遠,一些應用更傾向於這個近似值而不是ValueError。
異常是另一個技巧,幫助我們將程序細節劃分為模塊化的部分。在這個例子中,Python 的異常機制允許我們分離迭代改進的邏輯,它在try子句組中沒有發生改變,以及錯誤處理的邏輯,它出現在except子句中。我們也會發現,異常在使用 Python 實現解釋器時是個非常實用的特性。