8.錯誤和例外

到目前為止還沒有提到錯誤訊息,但如果你嘗試運行範例,你可能會發現一些錯誤訊息。常見的(至少)兩種不同的錯誤類別為:語法錯誤 (syntax error)例外 (exception)

8.1.語法錯誤 (Syntax Error)

語法錯誤又稱剖析錯誤 (parsing error),它或許是學習 Python 的過程最常聽見的抱怨:

>>>whileTrueprint('Hello world')  File"<stdin>", line1whileTrueprint('Hello world')^^^^^SyntaxError:invalid syntax

剖析器 (parser) 會重複犯錯的那一行,並用一個小箭頭指向該行檢測到錯誤的地方,但請注意這並非都會是應該去修改的地方。此例中,錯誤是在print() 函式中被檢測到,因為在它前面少了一個冒號 (':')。

檔案名稱(此例中為<stdin>)和列號會被印出來,所以如果訊息是來自一個檔案時,就可以知道去哪裡找問題。

8.2.例外 (Exception)

即使一段陳述式或運算式使用了正確的語法,嘗試執行時仍可能導致錯誤。執行時檢測到的錯誤稱為例外,例外不一定都很嚴重:你很快就能學會在 Python 程式中如何處理它們。不過大多數的例外不會被程式處理,並且會顯示如下的錯誤訊息:

>>>10*(1/0)Traceback (most recent call last):  File"<stdin>", line1, in<module>10*(1/0)~^~ZeroDivisionError:division by zero>>>4+spam*3Traceback (most recent call last):  File"<stdin>", line1, in<module>4+spam*3^^^^NameError:name 'spam' is not defined>>>'2'+2Traceback (most recent call last):  File"<stdin>", line1, in<module>'2'+2~~~~^~~TypeError:can only concatenate str (not "int") to str

錯誤訊息的最後一行指示發生了什麼事。例外有不同的類型,而類型名稱會作為訊息的一部份被印出。範例中的例外類型為:ZeroDivisionErrorNameErrorTypeError。作為例外類型被印出的字串,就是發生的內建例外 (built-in exception) 的名稱。所有的內建例外都是如此運作,但對於使用者自定的例外則不一定需要遵守(雖然這是一個有用的慣例)。標準例外名稱是內建的識別字 (identifier),不是保留關鍵字 (reserved keyword)。

此行其餘部分,根據例外的類型及導致例外的原因,說明例外的細節。

錯誤訊息的開頭,用堆疊回溯 (stack traceback) 的形式顯示發生例外的語境。一般來說,它含有一個列出源程式碼行 (source line) 的堆疊回溯;但它不會顯示從標準輸入中讀取的程式碼。

內建的例外章節列出內建的例外及它們的意義。

8.3.處理例外

編寫程式處理選定的例外是可行的。以下範例會要求使用者輸入內容,直到有效的整數被輸入為止,但它允許使用者中斷程式(使用Control-C 或作業系統支援的指令);請注意,由使用者產生的程式中斷會引發KeyboardInterrupt 例外信號。

>>>whileTrue:...try:...x=int(input("Please enter a number: "))...break...exceptValueError:...print("Oops!  That was no valid number.  Try again...")...

try 陳述式運作方式如下。

  • 首先,執行try 子句tryexcept 關鍵字之間的陳述式)。

  • 如果沒有發生例外,則except 子句會被跳過,try 陳述式執行完畢。

  • 如果執行try 子句時發生了例外,則該子句中剩下的部分會被跳過。如果例外的類型與except 關鍵字後面的例外名稱相符,則except 子句被執行,然後,繼續執行 try/except 區塊之後的程式碼。

  • 如果發生的例外未符合except 子句 中的例外名稱,則將其傳遞到外層的try 陳述式;如果仍無法找到處理者,則它是一個未處理例外 (unhandled exception),執行將停止,並顯示錯誤訊息。

try 陳述式可以有不只一個except 子句,為不同的例外指定處理者,而最多只有一個處理者會被執行。處理者只處理對應的 try 子句中發生的例外,而不會處理同一try 陳述式裡其他處理者內的例外。一個except 子句可以用一組括號內的 tuple 列舉多個例外,例如:

...except(RuntimeError,TypeError,NameError):...pass

except 子句中的一個類別符合該類別本身或它的衍生類別的例外(但不是反過來 -- 列出一個衍生類別的except 子句不會符合它的基底類別的例外)。例如,以下程式碼會按照 B、C、D 的順序印出:

classB(Exception):passclassC(B):passclassD(C):passforclsin[B,C,D]:try:raisecls()exceptD:print("D")exceptC:print("C")exceptB:print("B")

請注意,如果except 子句的順序被反轉(把exceptB 放到第一個),則會印出 B、B、B ­­——第一個符合的except 子句會被觸發。

當例外發生時,它可能有相關聯的值,也就是例外的引數。引數的存在與否及它的類型,是取決於例外的類型。

except 子句可以在例外名稱後面指定一個變數。這個變數被綁定到一個例外實例 (instance),其引數通常儲存在args 屬性中。為了方便,內建例外型別定義了__str__() 以印出所有引數而不需顯式地取用.args

>>>try:...raiseException('spam','eggs')...exceptExceptionasinst:...print(type(inst))# 例外的型別...print(inst.args)# 儲存在 .args 中的引數...print(inst)# __str__ 使得引數可以直接被印出,...# 但可能在例外的子類別中被覆蓋...x,y=inst.args# 解包引數...print('x =',x)...print('y =',y)...<class 'Exception'>('spam', 'eggs')('spam', 'eggs')x = spamy = eggs

例外的__str__() 輸出會被印在未處理例外訊息的最後一部分(「細節」)。

BaseException 是由全部的例外所共用的 base class。它的 subclass(子類別)之一,Exception,則是所有非嚴重例外 (non-fatal exception) 的 base class。有些例外不是Exception 的 subclass,而它們通常不會被處理,因為它們是用來指示程式應該終止。這些例外包括了由sys.exit() 所引發的SystemExit,以及當使用者想要中斷程式時所引發的KeyboardInterrupt

Exception 可以用作通配符 (wildcard) 來捕獲(幾乎)所有的例外。然而,比較好的做法是盡可能具體地說明我們打算處理的例外類型,並容許任何非預期例外的傳遞 (propagate)。

處理Exception 的最常見模式,是先將該例外印出或記錄,然後再重新引發它(也允許一個呼叫函式 (caller) 來處理該例外):

importsystry:f=open('myfile.txt')s=f.readline()i=int(s.strip())exceptOSErroraserr:print("OS error:",err)exceptValueError:print("Could not convert data to an integer.")exceptExceptionaserr:print(f"Unexpected{err=},{type(err)=}")raise

try ...except 陳述式有一個選擇性的else 子句,使用時,該子句必須放在所有except 子句之後。如果一段程式碼必須被執行,但try 子句又沒有引發例外時,這個子句很有用。例如:

forarginsys.argv[1:]:try:f=open(arg,'r')exceptOSError:print('cannot open',arg)else:print(arg,'has',len(f.readlines()),'lines')f.close()

使用else 子句比向try 子句添加額外的程式碼要好,因為這可以避免意外地捕獲不是由try ...except 陳述式保護的程式碼所引發的例外。

例外的處理者不僅處理try 子句內立即發生的例外,還處理try 子句內(即使是間接地)呼叫的函式內部發生的例外。例如:

>>>defthis_fails():...x=1/0...>>>try:...this_fails()...exceptZeroDivisionErroraserr:...print('Handling run-time error:',err)...Handling run-time error: division by zero

8.4.引發例外

raise 陳述式可讓程式設計師強制引發指定的例外。例如:

>>>raiseNameError('HiThere')Traceback (most recent call last):  File"<stdin>", line1, in<module>raiseNameError('HiThere')NameError:HiThere

raise 唯一的引數就是要引發的例外。該引數必須是一個例外實例或例外 class(衍生自BaseException 的 class,例如Exception 與它的 subclass)。如果一個例外 class 被傳遞,它會不含引數地呼叫它的建構函式 (constructor) ,使它被自動建立實例 (implicitly instantiated):

raiseValueError# 'raise ValueError()' 的簡寫

如果你只想判斷是否引發了例外,但並不打算處理它,則可以使用簡單的raise 陳述式來重新引發該例外:

>>>try:...raiseNameError('HiThere')...exceptNameError:...print('An exception flew by!')...raise...An exception flew by!Traceback (most recent call last):  File"<stdin>", line2, in<module>raiseNameError('HiThere')NameError:HiThere

8.5.例外鏈接 (Exception Chaining)

如果在except 段落內部發生了一個未處理的例外,則它會讓這個將要被處理的例外附加在後,並將其包含在錯誤訊息中:

>>>try:...open("database.sqlite")...exceptOSError:...raiseRuntimeError("unable to handle error")...Traceback (most recent call last):  File"<stdin>", line2, in<module>open("database.sqlite")~~~~^^^^^^^^^^^^^^^^^^^FileNotFoundError:[Errno 2] No such file or directory: 'database.sqlite'During handling of the above exception, another exception occurred:Traceback (most recent call last):  File"<stdin>", line4, in<module>raiseRuntimeError("unable to handle error")RuntimeError:unable to handle error

為了表明一個例外是另一個例外直接造成的結果,raise 陳述式容許一個選擇性的from 子句:

# exc 必須是例外實例或 None。raiseRuntimeErrorfromexc

要變換例外時,這種方式很有用。例如:

>>>deffunc():...raiseConnectionError...>>>try:...func()...exceptConnectionErrorasexc:...raiseRuntimeError('Failed to open database')fromexc...Traceback (most recent call last):  File"<stdin>", line2, in<module>func()~~~~^^  File"<stdin>", line2, infuncConnectionErrorThe above exception was the direct cause of the following exception:Traceback (most recent call last):  File"<stdin>", line4, in<module>raiseRuntimeError('Failed to open database')fromexcRuntimeError:Failed to open database

它也容許使用慣用語fromNone 來停用自動例外鏈接:

>>>try:...open('database.sqlite')...exceptOSError:...raiseRuntimeErrorfromNone...Traceback (most recent call last):  File"<stdin>", line4, in<module>raiseRuntimeErrorfromNoneRuntimeError

更多關於鏈接機制的資訊,詳見內建的例外

8.6.使用者自定的例外

程式可以透過建立新的例外 class 來命名自己的例外(深入了解 Python class,詳見Class(類別))。不論是直接還是間接地,例外通常應該從Exception class 衍生出來。

例外 class 可被定義來做任何其他 class 能夠做的事,但通常會讓它維持簡單,只提供一些屬性,讓關於錯誤的資訊可被例外的處理者抽取出來。

大多數的例外定義,都會以「Error」作為名稱結尾,類似於標準例外的命名。

許多標準模組會定義它們自己的例外,以報告在其定義的函式中發生的錯誤。

8.7.定義清理動作

try 陳述式有另一個選擇性子句,用於定義在所有情況下都必須被執行的清理動作。例如:

>>>try:...raiseKeyboardInterrupt...finally:...print('Goodbye, world!')...Goodbye, world!Traceback (most recent call last):  File"<stdin>", line2, in<module>raiseKeyboardInterruptKeyboardInterrupt

如果finally 子句存在,則finally 子句會是try 陳述式結束前執行的最後一項任務。不論try 陳述式是否產生例外,都會執行finally 子句。以下幾點將探討例外發生時,比較複雜的情況:

  • 若一個例外發生於try 子句的執行過程,則該例外會被某個except 子句處理。如果該例外沒有被except 子句處理,它會在finally 子句執行後被重新引發。

  • 一個例外可能發生於exceptelse 子句的執行過程。同樣地,該例外會在finally 子句執行後被重新引發。

  • 如果finally 子句執行breakcontinuereturn 陳述式,則例外不會被重新引發。

  • 如果try 陳述式遇到breakcontinuereturn 陳述式,則finally 子句會在執行breakcontinuereturn 陳述式之前先執行。

  • 如果finally 子句中包含return 陳述式,則回傳值會是來自finally 子句的return 陳述式的回傳值,而不是來自try 子句的return 陳述式的回傳值。

例如:

>>>defbool_return():...try:...returnTrue...finally:...returnFalse...>>>bool_return()False

另一個比較複雜的範例:

>>>defdivide(x,y):...try:...result=x/y...exceptZeroDivisionError:...print("division by zero!")...else:...print("result is",result)...finally:...print("executing finally clause")...>>>divide(2,1)result is 2.0executing finally clause>>>divide(2,0)division by zero!executing finally clause>>>divide("2","1")executing finally clauseTraceback (most recent call last):  File"<stdin>", line1, in<module>divide("2","1")~~~~~~^^^^^^^^^^  File"<stdin>", line3, individeresult=x/y~~^~~TypeError:unsupported operand type(s) for /: 'str' and 'str'

如你所見,finally 子句在任何情況下都會被執行。兩個字串相除所引發的TypeError 沒有被except 子句處理,因此會在finally 子句執行後被重新引發。

在真實應用程式中,finally 子句對於釋放外部資源(例如檔案或網路連線)很有用,無論該資源的使用是否成功。

8.8.預定義的清理動作

某些物件定義了在物件不再被需要時的標準清理動作,無論使用該物件的作業是成功或失敗。請看以下範例,它嘗試開啟一個檔案,並印出檔案內容至螢幕。

forlineinopen("myfile.txt"):print(line,end="")

這段程式碼的問題在於,執行完該程式碼後,它讓檔案在一段不確定的時間內處於開啟狀態。在簡單腳本中這不是問題,但對於較大的應用程式來說可能會是個問題。with 陳述式讓物件(例如檔案)在被使用時,能保證它們總是及時、正確地被清理。

withopen("myfile.txt")asf:forlineinf:print(line,end="")

陳述式執行完畢後,就算是在處理內容時遇到問題,檔案f 總是會被關閉。和檔案一樣,提供預定義清理動作的物件會在說明文件中表明這一點。

8.9.引發及處理多個無關的例外

在某些情況下,必須回報已經發生的多個例外。在並行框架 (concurrency framework) 中經常會出現這種情況,當平行的 (parallel) 某些任務可能已經失效,但還有其他用例 (use case) 希望能繼續執行並收集多個例外,而不是只有引發第一個例外時。

內建的ExceptionGroup 會包裝一個例外實例 (exception instance) 的 list(串列),使得它們可以一起被引發。由於它本身就是一個例外,因此它也可以像任何其他例外一樣被捕獲。

>>>deff():...excs=[OSError('error 1'),SystemError('error 2')]...raiseExceptionGroup('there were problems',excs)...>>>f()  + Exception Group Traceback (most recent call last):  |   File "<stdin>", line 1, in <module>  |     f()  |     ~^^  |   File "<stdin>", line 3, in f  |     raise ExceptionGroup('there were problems', excs)  | ExceptionGroup: there were problems (2 sub-exceptions)  +-+---------------- 1 ----------------    | OSError: error 1    +---------------- 2 ----------------    | SystemError: error 2    +------------------------------------>>>try:...f()...exceptExceptionase:...print(f'caught{type(e)}: e')...caught <class 'ExceptionGroup'>: e>>>

若使用except* 代替except,我們可以選擇性地只處理該群組中與特定類型匹配的例外。在以下範例中,展示了一個巢狀的例外群組 (exception group),每個except* 子句分別從該群組中提取一個特定類型的例外,同時讓所有其他的例外都傳遞到其他子句,最後再被重新引發。

>>>deff():...raiseExceptionGroup(..."group1",...[...OSError(1),...SystemError(2),...ExceptionGroup(..."group2",...[...OSError(3),...RecursionError(4)...]...)...]...)...>>>try:...f()...except*OSErrorase:...print("There were OSErrors")...except*SystemErrorase:...print("There were SystemErrors")...There were OSErrorsThere were SystemErrors  + Exception Group Traceback (most recent call last):  |   File "<stdin>", line 2, in <module>  |     f()  |     ~^^  |   File "<stdin>", line 2, in f  |     raise ExceptionGroup(  |     ...<12 lines>...  |     )  | ExceptionGroup: group1 (1 sub-exception)  +-+---------------- 1 ----------------    | ExceptionGroup: group2 (1 sub-exception)    +-+---------------- 1 ----------------      | RecursionError: 4      +------------------------------------>>>

請注意,被巢套在例外群組中的例外必須是實例,而不是類型。這是因為在實務上,這些例外通常是已經被程式引發並捕獲的例外,類似以下的模式:

>>>excs=[]...fortestintests:...try:...test.run()...exceptExceptionase:...excs.append(e)...>>>ifexcs:...raiseExceptionGroup("Test Failures",excs)...

8.10.用註解使例外更詳細

當一個例外是為了被引發而建立時,它通常會伴隨著一些資訊被初始化,這些資訊描述了當下發生的錯誤。在某些情況,在例外被捕獲之後添加資訊會很有用。為此,例外具有一個add_note(note) method(方法),它可以接受一個字串並將其添加到例外的註解清單中。標準的回溯呈現會在例外之後列出所有的註解,並按照其被添加的順序來排列。

>>>try:...raiseTypeError('bad type')...exceptExceptionase:...e.add_note('Add some information')...e.add_note('Add some more information')...raise...Traceback (most recent call last):  File"<stdin>", line2, in<module>raiseTypeError('bad type')TypeError:bad typeAdd some informationAdd some more information>>>

例如,在將例外收集到例外群組中時,我們可能希望為各個錯誤添加一些上下文的資訊。在以下範例中,群組中的每個例外都有一條註解,指示此錯誤是在何時發生。

>>>deff():...raiseOSError('operation failed')...>>>excs=[]>>>foriinrange(3):...try:...f()...exceptExceptionase:...e.add_note(f'Happened in Iteration{i+1}')...excs.append(e)...>>>raiseExceptionGroup('We have some problems',excs)  + Exception Group Traceback (most recent call last):  |   File "<stdin>", line 1, in <module>  |     raise ExceptionGroup('We have some problems', excs)  | ExceptionGroup: We have some problems (3 sub-exceptions)  +-+---------------- 1 ----------------    | Traceback (most recent call last):    |   File "<stdin>", line 3, in <module>    |     f()    |     ~^^    |   File "<stdin>", line 2, in f    |     raise OSError('operation failed')    | OSError: operation failed    | Happened in Iteration 1    +---------------- 2 ----------------    | Traceback (most recent call last):    |   File "<stdin>", line 3, in <module>    |     f()    |     ~^^    |   File "<stdin>", line 2, in f    |     raise OSError('operation failed')    | OSError: operation failed    | Happened in Iteration 2    +---------------- 3 ----------------    | Traceback (most recent call last):    |   File "<stdin>", line 3, in <module>    |     f()    |     ~^^    |   File "<stdin>", line 2, in f    |     raise OSError('operation failed')    | OSError: operation failed    | Happened in Iteration 3    +------------------------------------>>>