# 第八章 異常
> 來源:http://www.cnblogs.com/Marlowes/p/5428641.html
> 作者:Marlowes
在編寫程序的時候,程序員通常需要辨別事件的正常過程和異常(非正常)的情況。這類異常事件可能是錯誤(比如試圖除以`0`),或者是不希望經常發生的事情。為了能夠處理這些異常事件,可以在所有可能發生這類事件的地方都使用條件語句(比如讓程序檢查除法的分母是否為零)。但是,這么做可能不僅會沒效率和不靈活,而且還會讓程序難以閱讀。你可能會想直接忽略這些異常事件,期望它們永不發生,但Python的異常對象提供了非常強大的替代解決方案。
本章介紹如何創建和引發自定義的異常,以及處理異常的各種方法。
## 8.1 什么是異常
Python用_異常對象_(exception object)來表示異常情況。遇到錯誤后,會引發異常。如果異常對象并未被處理或捕捉,程序就會用所謂的_回溯_(traceback, 一種錯誤信息)終止執行:
```
>>> 1 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
```
如果這些錯誤信息就是異常的全部功能,那么它也就不必存在了。事實上,每個異常都是一些類(本例中是`ZeroDivisionError`)的實例,這些實例可以被引發,并且可以用很多種方法進行捕捉,使得程序可以捉住錯誤并且對其進行處理,而不是讓整個程序失效。
## 8.2 按自己的方式出錯
異常可以在某些東西出錯的時候自動引發。在學習如何處理異常之前,先看一下自己如何引發異常,以及創建自己的異常類型。
### 8.2.1 `raise`語句
為了引發異常,可以使用一個類(應該是`Exception`的子類)或者實例參數調用`raise`語句。使用類時,程序會自動創建類的一個實例。下面是一些簡單的例子,使用了內建的`Exception`的異常類:
```
>>> raise Exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception
>>> raise Exception("hyperdrive overload")
Traceback (most recent call last):
File "<stdin>", line 1, in <module> \
Exception: hyperdrive overload
```
第一個例子raise Exception引發了一個沒有任何有關錯誤信息的普通異常。后一個例子中,則添加了錯誤信息hyperdrive overload。
內建的異常類有很多。Python庫參考手冊的Built-in Exceptions一節中有關與它們的描述。用交互式解釋器也可以分析它們,這些內建異常都可以在`exceptions`模塊(和內建的命名空間)中找到。可以使用`dir`函數列出模塊內容,這部分會在第十章中講到:
```
>>> import exceptions
>>> dir(exceptions)
['ArithmeticError', 'AssertionError', 'AttributeError', ...]
```
讀者的解釋器中,這個名單可能要長得多——出于對易讀性的考慮,這里刪除了大部分名字,所有這些異常都可以用在`raise`語句中:
```
>>> raise ArithmeticError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ArithmeticError
```
表8-1描述了一些最重要的內建異常類:
表8-1 一些內建異常類
```
Exception 所有異常的基類
AttributeError 特性引用或賦值失敗時引發
IOError 試圖打開不存在文件(包括其他情況)時引發
IndexError ? ? ? ? ? ? ? ? ? ? ? 在使用序列中不存在的索引時引發
KeyError ?在使用映射中不存在的鍵時引發
NameError ? 在找不到名字(變量)時引發
SyntaxError 在代碼為錯誤形式時引發
TypeError 在內建操作或者函數應用于錯誤類型的對象時引發
ValueError 在內建操作或者函數應用于正確類型的對象,但是該對象使用不合適的值時引發
ZeroDivisionError 在除法或者模除操作的第二個參數為0時引發
```
### 8.2.2 自定義異常類
盡管內建的異常類已經包括了大部分的情況,而且對于很多要求都已經足夠了,但是有些時候還是需要創建自己的異常類。比如在超光速推進裝置過載(hyperdrive overload)的例子中,如果能有個具體的`HyperDriveError`類來表示超光速推進裝置的錯誤狀況是不是更自然一些?錯誤信息是足夠了,但是會在8.3節中看到,可以根據異常所在的類,選擇性地處理當前類型的異常。所以如果想要使用特殊的錯誤處理代碼處理超光速推進裝置的錯誤,那么就需要一個獨立于`exceptions`模塊的異常類。
那么如何創建自己的異常類呢?就像其他類一樣,只是要確保從`Exception`類繼承(不管是間接還是直接,也就是說繼承其他的內建異常類也是可以的)。那么編寫一個自定義異常類基本上就像下面這樣:
```
class SomeCustomException(Exception):
pass
```
還不能做太多事,對吧?(如果你愿意,也可以向你的異常類中增加方法)
## 8.3 捕捉異常
前面曾經提到過,關于異常的最有意思的地方就是可以處理它們(通常叫做誘捕或者捕捉異常)。這個功能可以使用`try/except`語句來實現。假設創建了一個讓用戶輸入兩個數,然后進行相除的程序,像下面這樣:
```
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
```
程序工作正常,假如用戶輸入0作為第二個數
```
Enter the first number: 10 Enter the second number: 0
Traceback (most recent call last):
File "/home/marlowes/MyPython/My_Exception.py", line 6, in <module>
print x / y
ZeroDivisionError: integer division or modulo by zero
```
為了捕捉異常并且做出一些錯誤處理(本例中只是輸出一些更友好的錯誤信息),可以這樣重寫程序:
```
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
except ZeroDivisionError:
print "The second number can't be zero!"
```
看起來用`if`語句檢查`y`值會更簡單一些,本例中這樣做的確很好。但是如果需要給程序加入更多除法,那么就得給每個除法加個if語句。而且使用`try/except`的話只需要一個錯誤處理器。
_注:如果沒有捕捉異常,它就會被“傳播”到調用的函數中。如果在那里依然沒有捕獲,這些異常就會“浮”到程序的最頂層,也就是說你可以捕捉到在其他人的函數中所引發的異常。有關這方面的更多信息,請參見8.10節。_
**看,沒參數**
如果捕捉到了異常,但是又想重新引發它(也就是說要傳遞異常,不進行處理),那么可以調用不帶參數的`raise`(還能在捕捉到異常時顯式地提供具體異常,在8.6節會對此進行解釋)。
舉個例子吧,看看這么做多么有用:考慮一下一個能“屏蔽”`ZeroDivisionError`(除零錯誤)的計算器類。如果這個行為被激活,那么計算器就打印錯誤信息,而不是讓異常傳播。如果在與用戶交互的過程中使用,那么這就有用了,但是如果是在程序內部使用,引發異常會更好些。因此“屏蔽”機制就可以關掉了,下面是這樣一個類的代碼:
```
class MuffledCalculator():
muffled = False
def calc(self, expr):
try:
return eval(expr)
except ZeroDivisionError:
if self.muffled:
print "Division by zero is illegal"
else:
raise
```
_注:如果除零行為發生而屏蔽機制被打開,那么`calc`方法會(隱式地)返回`None`。換句話說,如果打開了屏蔽機制,那么就不應該依賴返回值。_
下面是這個類的用法示例,分別打開和關閉了屏蔽:
```
>>> calculator = MuffledCalculator()
>>> calculator.calc("10 / 2")
5
>>> calculator.calc("10 / 0")
Traceback (most recent call last):
File "/home/marlowes/MyPython/My_Exception.py", line 28, in <module> calculator.calc("10 / 0")
File "/home/marlowes/MyPython/My_Exception.py", line 19, in calc return eval(expr)
File "<string>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero >>> calculator.muffled = True >>> calculator.calc("10 / 0")
Division by zero is illegal
```
當計算器沒有打開屏蔽機制時,`ZeroDivisionError`被捕捉但已傳遞了。
## 8.4 不止一個`except`子句
如果運行上一節的程序并且在提示符后面輸入非數字類型的值,就會產生另一個異常:
```
Enter the first number: 10
Enter the second number: "Hello, world!"
Traceback (most recent call last):
File "/home/marlowes/MyPython/My_Exception.py", line 8, in <module>
print x / y
TypeError: unsupported operand type(s) for /: 'int' and 'str'
```
因為`except`子句只尋找`ZeroDivisionError`異常,這次的錯誤就溜過了檢查并導致程序終止。為了捕捉這個異常,可以直接在同一個`try/except`語句后面加上另一個`except`子句:
```
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
except ZeroDivisionError:
print "The second number can't be zero!"
except TypeError:
print "That wasn't a number, was it?"
```
這次用`if`語句實現可就復雜了。怎么檢查一個值是否能被用在除法中?方法很多,但是目前最好的方式是直接將值用來除一下看看是否奏效。
還應該注意到,異常處理并不會搞亂原來的代碼,而增加一大堆`if`語句檢查可能的錯誤情況會讓代碼相當難讀。
## 8.5 用一個塊捕捉兩個異常
如果需要用一個塊捕捉多個類型異常,那么可以將它們作為元組列出,像下面這樣:
```
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
except (ZeroDivisionError, TypeError, NameError):
print "Your numbers were bogus..."
```
上面的代碼中,如果用戶輸入字符串或者其他類型的值,而不是數字,或者第2個數為0,都會打印同樣的錯誤信息。當然,只打印一個錯誤信息并沒有什么幫助。另外一個方法就是繼續要求輸入數字直到可以進行除法運算為止。8.8節中會介紹如何實現這一功能。
注意,`except`子句中異常對象外面的圓括號很重要。忽略它們是一種常見的錯誤,那樣你會得不到想要的結果。關于這方面的解釋,請參見8.6節。
## 8.6 捕捉對象
如果希望在`except`子句中訪問異常對象本身,可以使用兩個參數(注意,就算要捕捉到多個異常,也只需向`except`子句提供一個參數——一個元組)。比如,如果想讓程序繼續運行,但是又因為某種原因想記錄下錯誤(比如只是打印給用戶看),這個功能就很有用。下面的示例程序會打印異常(如果發生的話),但是程序會繼續運行:
```
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
except (ZeroDivisionError, TypeError), e:
print e
```
(在這個小程序中,`except`子句再次捕捉了兩種異常,但是因為你可以顯式地捕捉對象本身,所以異常可以打印出來,用戶就能看到發生什么(8.8節會介紹一個更有用的方法)。——譯者注)
_注:在Python3.0中,`except`子句會被寫作`except (ZeroDivisionError, TypeError) as e`。_
## 8.7 真正的捕捉
就算程序能處理好幾種類型的異常,但是有些異常還會從眼皮地下溜走。比如還用那個除法程序來舉例,在提示符下面直接按回車,不輸入任何東西,會的到一個類似下面這樣的錯誤信息(_棧跟蹤_):
```
Traceback (most recent call last):
File "/home/marlowes/MyPython/My_Exception.py", line 33, in <module> x = input("Enter the first number: ")
File "<string>", line 0
^ SyntaxError: unexpected EOF while parsing
```
這個異常逃過了`try/except`語句的檢查,這很正常。程序員無法預測會發生什么,也不能對其進行準備。在這些情況下,與其用那些并非捕捉這些異常的`try/except`語句隱藏異常,還不如讓程序立刻崩潰。
但是如果真的想用一段代碼捕捉所有異常,那么可以在except子句中忽略所有的異常類:
```
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
print x / y
except:
print "Something wrong happened..."
```
現在可以做任何事情了:
```
Enter the first number: "This" is *completely* illegal 123 Something wrong happened...
```
_警告:像這樣捕捉所有異常是危險的,因為它會隱藏所有程序員未想到并且未做好準備處理的錯誤。它同樣會捕捉用戶終止執行的Ctrl+C企圖,以及用`sys.exit`函數終止程序的企圖,等等。這時使用`except Exception, e`會更好些,或者對異常對象`e`進行一些檢查。_
## 8.8 萬事大吉
有些情況中,沒有壞事發生時執行一段代碼是很有用的;可以像對條件和循環語句那樣,給`try/except`語句加個`else`子句:
```
try:
print "A simple task"
except:
print "What? Something went wrong?"
else:
print "Ah... It went as planned."
```
運行之后會的到如下輸出:
```
A simple task
Ah... It went as planned.
```
使用`else`子句可以實現在8.5節中提到的循環:
```
while True:
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
value = x / y
print "x / y is", value
except:
print "Invalid input. Please try again."
else:
break
```
這里的循環只有在沒有異常引發的情況下才會退出(由`else`子句中的`break`語句退出)。換句話說,只要有錯誤發生,程序會不斷要求重新輸入。下面是一個例子的運行情況:
```
Enter the first number: 1
Enter the second number: 0
Invalid input. Please try again.
Enter the first number: "foo"
Enter the second number: "bar"
Invalid input. Please try again.
Enter the first number: baz
Invalid input. Please try again.
Enter the first number: 10
Enter the second number: 2
x / y is 5
```
之前提到過,可以使用空的`except`子句來捕捉所有`Exception`類的異常(也會捕捉其所有子類的異常)。百分之百捕捉到所有的異常是不可能的,因為`try/except`語句中的代碼可能會出現問題,比如使用舊風格的字符串異常或者自定義的異常類不是`Exception`類的子類。不過如果需要使用`except Exception`的話,可以使用8.6節中的技巧在除法程序中打印更加有用的錯誤信息:
```
while True:
try:
x = input("Enter the first number: ")
y = input("Enter the second number: ")
value = x / y
print "x / y is", value
except Exception, e:
print "Invalid input:", e
print "Please try again"
else:
break
```
下面是示例運行:
```
Enter the first number: 1 Enter the second number: 0
Invalid input: integer division or modulo by zero
Please try again
Enter the first number: "x"
Enter the second number: "y"
Invalid input: unsupported operand type(s) for /: 'str' and 'str' Please try again
Enter the first number: quuux
Invalid input: name 'quuux' is not defined
Please try again
Enter the first number: 10
Enter the second number: 2
x / y is 5
```
## 8.9 最后······
最后,是`finally`子句。它可以用來在可能的異常后進行清理。它和`try`子句聯合使用:
```
x = None
try:
x = 1 / 0
finally:
print "Cleaning up..."
del x
```
上面的代碼中,`finally`子句肯定會被執行,不管`try`子句中是否發生異常(在`try`子句之前初始化`x`的原因是如果不這樣做,由于`ZeroDivisionError`的存在,`x`就永遠不會被賦值。這樣就會導致在`finally`子句中使用`del`刪除它的時候產生異常,而且這個異常是無法捕捉的)。
運行這段代碼,在程序崩潰之前,對于變量`x`的清理就完成了:
```
Cleaning up...
File "/home/marlowes/MyPython/My_Exception.py", line 36, in <module> x = 1 / 0
ZeroDivisionError: integer division or modulo by zero
```
_注:在Python2.5之前的版本內,`finally`子句需要獨立使用,而不能作為`try`語句的`except`子句使用。如果都要使用的話,那么需要兩條語句。但在Python2.5及其之后的版本中,可以盡情地組合這些子句。_
## 8.10 異常和函數
異常和函數能很自然地一起工作。如果異常在函數內引發而不被處理,它就會傳播至(浮到)函數調用的地方。如果在那里也沒有處理異常,它就會繼續傳播,一直到達主程序(全局作用域)。如果那里沒有異常處理程序,程序會帶著棧跟蹤中止。看個例子:
```
>>> def faulty():
... raise Exception("Something is wrong")
...
>>> def ignore_exception():
... faulty()
...
>>> def handle_exception():
... try:
... faulty()
... except:
... print "Exception handled"
...
>>> ignore_exception()
Traceback (most recent call last):
File "<stdin>", line 1, in <module> File "<stdin>", line 2, in ignore_exception
File "<stdin>", line 2, in faulty
Exception: Something is wrong
>>> handle_exception()
Exception handled
```
可以看到,`faulty`中產生的異常通過`faulty`和`ignore_exception`傳播,最終導致了棧跟蹤。同樣地,它也傳播到了`handle_exception`,但在這個函數中被`try/except`語句處理。
## 8.11 異常之禪
異常處理并不是很復雜。如果知道某段代碼可能會導致某種異常,而又不希望程序以堆棧跟蹤的形式終止,那么就根據需要添加`try/except`或者`try/finally`語句(或者它們的組合)進行處理。
有些時候,條件語句可以實現和異常處理同樣的功能,但是條件語句可能在自然性和可讀性上差些。而從另一方面來看,某些程序中使用`if/else`實現會比使用`try/except`要好。讓我們看幾個例子。
假設有一個字典,我們希望打印出存儲在特定的鍵下面的值。如果該鍵不存在,那么什么也不做。代碼可能像下面這樣寫:
```
def describePerson(person):
print "Description of", person["name"]
print "Age:", person["age"]
if "occupation" in person:
print "Occupation:", person["occupation"]
```
如果給程序提供包含名字`Throatwobbler Mangrove`和年齡`42`(沒有職業)的字典的函數,會得到如下輸出:
```
Description of Throatwobbler Mangrove
Age: 42
```
如果添加了職業`camper`,會的到如下輸出:
```
Description of Throatwobbler Mangrove
Age: 42
Occupation: camper
```
代碼非常直觀,但是效率不高(盡管這里主要關心的是代碼的簡潔性)。程序會兩次查找`"occupation"`鍵,其中一次用來檢查鍵是否存在(在條件語句中),另外一次獲得值(打印)。另外一個解決方案如下:
```
def describePerson(person):
print "Description of", person["name"]
print "Age:", person["age"]
try:
print "Occupation: " + person["occupation"]
except KeyError:
pass
```
_注:這里在打印職業時使用加號而不是逗號。否則字符串`"Occupation:"`在異常引發之前就會被輸出。_
這個程序直接假定`"occupation"`鍵存在。如果它的確存在,那么就會省事一些。直接取出它的值再打印輸出即可——不用額外檢查它是否真的存在。如果該鍵不存在,則會引發`KeyError`異常,而被`except`子句捕捉到。
在查看對象是否存在特定特性時,`try/except`也很有用。假設想要查看某對象是否有`write`特性,那么可以使用如下代碼:
```
try:
obj.write
except AttributeError:
print "The object is not writeable"
else:
print "The object is writeable"
```
這里的`try`子句僅僅訪問特性而不用對它做別的有用的事情。如果`AttributeError`異常引發,就證明對象沒有這個特性,反之存在該特性。這是實現第七章中介紹的`getattr`(7.2.8節)方法的替代方法,至于更喜歡哪種方法,完全是個人喜好。其實在內部實現`getattr`時也是使用這種方法:它試著訪問特性并且捕捉可能引發的`AttributeError`異常。
注意,這里所獲得的效率提高并不多(微乎其微),一般來說(除非程序有性能問題)程序開發人員不用過多擔心這類優化問題。在很多情況下,使用`try/except`語句比使用`if/else`會更自然一些(更“Python化”),應該養成盡可能使用`try/except`語句的習慣。
## 8.12 小結
本章的主題如下。
? 異常對象:異常情況(比如發生錯誤)可以用異常對象表示。它們可以用幾種方法處理,但是如果忽略的話,程序就會中止。
? 警告:警告類似于異常,但是(一般來說)僅僅打印錯誤信息。
? 引發異常:可以使用`raise`語句引發異常。它接受異常類或者異常實例作為參數。還能提供兩個參數(異常和錯誤信息)。如果在except子句中不使用參數調用`raise`,它就會“重新引發”該子句捕捉到的異常。
? 自定義異常類:用繼承`Exception`類的方法可以創建自己的異常類。
? 捕捉異常:使用`try`語句的`except`子句捕捉異常。如果在`except`子句中不特別指定異常類,那么所有的異常都會被捕捉。異常可以放在元組中以實現多個異常的指定。如果給`except`提供兩個參數,第二個參數就會綁定到異常對象上。同樣,在一個`try/except`語句中能包含多個`except`子句,用來分別處理不同的異常。
? `else`子句:除了`except`子句,可以使用`else`子句。如果主`try`塊中沒有引發異常,`else`子句就會被執行。
? `finally`:如果需要確保某些代碼不管是否有異常引發都要執行(比如清理代碼),那么這些代碼可以放置在`finally`(注意,在Python2.5以前,在一個`try`語句中不能同時使用`except`和`finally`子句——但是一個子句可以放置在另一個子句中)子句中。
? 異常和函數:在函數內引發異常時,它就會被傳播到函數調用的地方(對于方法也是一樣)。
### 8.12.1 本章的新函數
本章涉及的新函數如表8-2所示。
表8-2 本章的新函數
```
warnings,filterwarnings(action, ...) 用于過濾警告
```
### 8.12.2 接下來學什么
本章講異常,內容可能有些意外(雙關語),而下一章的內容真的很不可思議,恩,近乎不可思議。