8. 錯(cuò)誤和異常?直到現(xiàn)在,我們還沒有更多的提及錯(cuò)誤信息,但是如果你真的嘗試了前面的例子,也許你已經(jīng)見到過一些。Python(至少)有兩種錯(cuò)誤很容易區(qū)分:語法錯(cuò)誤 和異常。 8.1. 語法錯(cuò)誤?語法錯(cuò)誤,或者稱之為解析錯(cuò)誤,可能是你在學(xué)習(xí) Python 過程中最煩的一種: >>>
>>> while True print('Hello world') File "<stdin>", line 1, in ? while True print('Hello world') ^ SyntaxError: invalid syntax 語法分析器指出了出錯(cuò)的一行,并且在最先找到的錯(cuò)誤的位置標(biāo)記了一個(gè)小小的’箭頭’。錯(cuò)誤是由箭頭前面 的標(biāo)記引起的(至少檢測(cè)到是這樣的): 在這個(gè)例子中,檢測(cè)到錯(cuò)誤發(fā)生在函數(shù)print(),因?yàn)樵谒叭鄙僖粋€(gè)冒號(hào)(':')。文件名和行號(hào)會(huì)一并輸出,所以如果運(yùn)行的是一個(gè)腳本你就知道去哪里檢查錯(cuò)誤了。 8.2. 異常?即使一條語句或表達(dá)式在語法上是正確的,在運(yùn)行它的時(shí)候,也有可能發(fā)生錯(cuò)誤。在執(zhí)行期間檢測(cè)到的錯(cuò)誤被稱為異常 并且程序不會(huì)無條件地崩潰:你很快就會(huì)知道如何在 Python 程序中處理它們。然而大多數(shù)異常都不會(huì)被程序處理,導(dǎo)致產(chǎn)生類似下面的錯(cuò)誤信息: >>>
>>> 10 * (1/0) Traceback (most recent call last): File "<stdin>", line 1, in ? ZeroDivisionError: division by zero >>> 4 + spam*3 Traceback (most recent call last): File "<stdin>", line 1, in ? NameError: name 'spam' is not defined >>> '2' + 2 Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: Can't convert 'int' object to str implicitly 最后一行的錯(cuò)誤消息指示發(fā)生了什么事。異常有不同的類型,其類型會(huì)作為消息的一部分打印出來:在這個(gè)例子中的類型有ZeroDivisionError、 NameError和TypeError。打印出來的異常類型的字符串就是內(nèi)置的異常的名稱。這對(duì)于所有內(nèi)置的異常是正確的,但是對(duì)于用戶自定義的異常就不一定了(盡管這是非常有用的慣例)。標(biāo)準(zhǔn)異常的名稱都是內(nèi)置的標(biāo)識(shí)符(不是保留的關(guān)鍵字)。 這一行最后一部分給出了異常的詳細(xì)信息和引起異常的原因。 錯(cuò)誤信息的前面部分以堆?;厮莸男问斤@示了異常發(fā)生的上下文。通常調(diào)用棧里會(huì)包含源代碼的行信息,但是來自標(biāo)準(zhǔn)輸入的源碼不會(huì)顯示行信息。 內(nèi)置的異常 列出了內(nèi)置的異常以及它們的含義。 8.3. 拋出異常?可以通過編程來選擇處理部分異常。看一下下面的例子,它會(huì)一直要求用戶輸入直到輸入一個(gè)合法的整數(shù)為止,但允許用戶中斷這個(gè)程序(使用Control-C或系統(tǒng)支持的任何方法);注意用戶產(chǎn)生的中斷引發(fā)的是 KeyboardInterrupt 異常。 >>>
>>> while True: ... try: ... x = int(input("Please enter a number: ")) ... break ... except ValueError: ... print("Oops! That was no valid number. Try again...") ... Try語句按以下方式工作。
Try語句可能有多個(gè)子句,以指定不同的異常處理程序。不過至多只有一個(gè)處理程序?qū)⒈粓?zhí)行。處理程序只處理發(fā)生在相應(yīng) try 子句中的異常,不會(huì)處理同一個(gè)try字句的其他處理程序中發(fā)生的異常。一個(gè) except 子句可以用帶括號(hào)的元組列出多個(gè)異常的名字,例如: ... except (RuntimeError, TypeError, NameError): ... pass 最后一個(gè) except 子句可以省略異常名稱,以當(dāng)作通配符使用。使用這種方式要特別小心,因?yàn)樗鼤?huì)隱藏一個(gè)真實(shí)的程序錯(cuò)誤!它還可以用來打印一條錯(cuò)誤消息,然后重新引發(fā)異常 (讓調(diào)用者也去處理這個(gè)異常): import sys try: f = open('myfile.txt') s = f.readline() i = int(s.strip()) except OSError as err: print("OS error: {0}".format(err)) except ValueError: print("Could not convert data to an integer.") except: print("Unexpected error:", sys.exc_info()[0]) raise try...except語句有一個(gè)可選的else 子句,其出現(xiàn)時(shí),必須放在所有 except 子句的后面。如果需要在 try 語句沒有拋出異常時(shí)執(zhí)行一些代碼,可以使用這個(gè)子句。例如: for arg in sys.argv[1:]: try: f = open(arg, 'r') except IOError: print('cannot open', arg) else: print(arg, 'has', len(f.readlines()), 'lines') f.close() 使用 else子句比把額外的代碼放在try子句中要好,因?yàn)樗梢员苊庖馔獠东@不是由try ...保護(hù)的代碼所引發(fā)的異常。除了語句。 當(dāng)異常發(fā)生時(shí),它可能帶有相關(guān)數(shù)據(jù),也稱為異常的參數(shù)。參數(shù)的有無和類型取決于異常的類型。 except 子句可以在異常名之后指定一個(gè)變量。這個(gè)變量將綁定于一個(gè)異常實(shí)例,同時(shí)異常的參數(shù)將存放在實(shí)例的args中。為方便起見,異常實(shí)例定義了__str__() ,因此異常的參數(shù)可以直接打印而不必引用.args。也可以在引發(fā)異常之前先實(shí)例化一個(gè)異常,然后向它添加任何想要的屬性。 >>>
>>> try: ... raise Exception('spam', 'eggs') ... except Exception as inst: ... print(type(inst)) # the exception instance ... print(inst.args) # arguments stored in .args ... print(inst) # __str__ allows args to be printed directly, ... # but may be overridden in exception subclasses ... x, y = inst.args # unpack args ... print('x =', x) ... print('y =', y) ... <class 'Exception'> ('spam', 'eggs') ('spam', 'eggs') x = spam y = eggs 對(duì)于未處理的異常,如果它含有參數(shù),那么參數(shù)會(huì)作為異常信息的最后一部分打印出來。 異常處理程序不僅處理直接發(fā)生在 try 子句中的異常,而且還處理 try 子句中調(diào)用的函數(shù)(甚至間接調(diào)用的函數(shù))引發(fā)的異常。例如: >>>
>>> def this_fails(): ... x = 1/0 ... >>> try: ... this_fails() ... except ZeroDivisionError as err: ... print('Handling run-time error:', err) ... Handling run-time error: int division or modulo by zero 8.4. 引發(fā)異常?raise語句允許程序員強(qiáng)行引發(fā)一個(gè)指定的異常。例如: >>>
>>> raise NameError('HiThere') Traceback (most recent call last): File "<stdin>", line 1, in ? NameError: HiThere raise的唯一參數(shù)指示要引發(fā)的異常。它必須是一個(gè)異常實(shí)例或異常類(從Exception派生的類)。 如果你確定需要引發(fā)異常,但不打算處理它,一個(gè)簡(jiǎn)單形式的raise語句允許你重新引發(fā)異常: >>>
>>> try: ... raise NameError('HiThere') ... except NameError: ... print('An exception flew by!') ... raise ... An exception flew by! Traceback (most recent call last): File "<stdin>", line 2, in ? NameError: HiThere 8.5. 用戶定義的異常?程序可以通過創(chuàng)建新的異常類來命名自己的異常(Python 類的更多內(nèi)容請(qǐng)參見類)。異常通常應(yīng)該繼承Exception類,直接繼承或者間接繼承都可以。例如: >>>
>>> class MyError(Exception): ... def __init__(self, value): ... self.value = value ... def __str__(self): ... return repr(self.value) ... >>> try: ... raise MyError(2*2) ... except MyError as e: ... print('My exception occurred, value:', e.value) ... My exception occurred, value: 4 >>> raise MyError('oops!') Traceback (most recent call last): File "<stdin>", line 1, in ? __main__.MyError: 'oops!' 在此示例中,Exception默認(rèn)的__init__()被覆蓋了。新的行為簡(jiǎn)單地創(chuàng)建了value 屬性。這將替換默認(rèn)的創(chuàng)建args 屬性的行為。 異常類可以像其他類一樣做任何事情,但是通常都會(huì)比較簡(jiǎn)單,只提供一些屬性以允許異常處理程序獲取錯(cuò)誤相關(guān)的信息。創(chuàng)建一個(gè)能夠引發(fā)幾種不同錯(cuò)誤的模塊時(shí),一個(gè)通常的做法是為該模塊定義的異常創(chuàng)建一個(gè)基類,然后基于這個(gè)基類為不同的錯(cuò)誤情況創(chuàng)建特定的子類: class Error(Exception): """Base class for exceptions in this module.""" pass class InputError(Error): """Exception raised for errors in the input. Attributes: expression -- input expression in which the error occurred message -- explanation of the error """ def __init__(self, expression, message): self.expression = expression self.message = message class TransitionError(Error): """Raised when an operation attempts a state transition that's not allowed. Attributes: previous -- state at beginning of transition next -- attempted new state message -- explanation of why the specific transition is not allowed """ def __init__(self, previous, next, message): self.previous = previous self.next = next self.message = message 大多數(shù)異常的名字都以"Error"結(jié)尾,類似于標(biāo)準(zhǔn)異常的命名。 很多標(biāo)準(zhǔn)模塊中都定義了自己的異常來報(bào)告在它們所定義的函數(shù)中可能發(fā)生的錯(cuò)誤。類 這一章給出了類的詳細(xì)信息。 8.6. 定義清理操作?Try語句有另一個(gè)可選的子句,目的在于定義必須在所有情況下執(zhí)行的清理操作。例如: >>>
>>> try: ... raise KeyboardInterrupt ... finally: ... print('Goodbye, world!') ... Goodbye, world! KeyboardInterrupt 不管有沒有發(fā)生異常,在離開try語句之前總是會(huì)執(zhí)行finally子句。當(dāng)try子句中發(fā)生了一個(gè)異常,并且沒有except字句處理(或者異常發(fā)生在try或else子句中),在執(zhí)行完finally子句后將重新引發(fā)這個(gè)異常。try語句由于break、contine或return語句離開時(shí),同樣會(huì)執(zhí)行finally子句。下面是一個(gè)更復(fù)雜些的例子: >>>
>>> def divide(x, y): ... try: ... result = x / y ... except ZeroDivisionError: ... print("division by zero!") ... else: ... print("result is", result) ... finally: ... print("executing finally clause") ... >>> divide(2, 1) result is 2.0 executing finally clause >>> divide(2, 0) division by zero! executing finally clause >>> divide("2", "1") executing finally clause Traceback (most recent call last): File "<stdin>", line 1, in ? File "<stdin>", line 3, in divide TypeError: unsupported operand type(s) for /: 'str' and 'str' 正如您所看到的,在任何情況下都會(huì)執(zhí)行finally子句。由兩個(gè)字符串相除引發(fā)的 TypeError異常沒有被except子句處理,因此在執(zhí)行finally子句后被重新引發(fā)。 在真實(shí)的應(yīng)用程序中, finally子句用于釋放外部資源(例如文件或網(wǎng)絡(luò)連接),不管資源的使用是否成功。 8.7. 清理操作的預(yù)定義?有些對(duì)象定義了在不需要該對(duì)象時(shí)的標(biāo)準(zhǔn)清理操作,無論該對(duì)象的使用是成功還是失敗。看看下面的示例,它嘗試打開一個(gè)文件并打印其內(nèi)容到屏幕。 for line in open("myfile.txt"): print(line, end="") 這段代碼的問題就是這部分代碼執(zhí)行完之后它還會(huì)讓文件在一段不確定的時(shí)間內(nèi)保持打開狀態(tài)。這在簡(jiǎn)單的腳本中沒什么,但是在大型應(yīng)用程序中可能是一個(gè)問題。 With語句可以確保像文件這樣的對(duì)象總能及時(shí)準(zhǔn)確地被清理掉。 with open("myfile.txt") as f: for line in f: print(line, end="") 執(zhí)行該語句后,文件f 將始終被關(guān)閉,即使在處理某一行時(shí)遇到了問題。提供預(yù)定義的清理行為的對(duì)象,和文件一樣,會(huì)在它們的文檔里說明。 |
|