本文內容全部出自《Python基礎教程》第二版,在此分享自己的學習之路。 lxx___歡迎轉載:http://www.cnblogs.com/Marlowes/p/5428641.htmllxx___ Created on Marlowes 在編寫程式的時候,程式員通常需要辨別事件的正常過程和異常( ...
本文內容全部出自《Python基礎教程》第二版,在此分享自己的學習之路。
______歡迎轉載:http://www.cnblogs.com/Marlowes/p/5428641.html______
Created on 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 接下來學什麼
本章講異常,內容可能有些意外(雙關語),而下一章的內容真的很不可思議,恩,近乎不可思議。