8. 錯誤和例外
*************

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


8.1. 語法錯誤 (Syntax Error)
============================

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

   >>> while True print('Hello world')
     File "<stdin>", line 1
       while True print('Hello world')
                  ^^^^^
   SyntaxError: invalid syntax

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

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


8.2. 例外 (Exception)
=====================

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

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

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

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

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

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


8.3. 處理例外
=============

編寫程式處理選定的例外是可行的。以下範例會要求使用者輸入內容，直到有效
的整數被輸入為止，但它允許使用者中斷程式（使用 "Control"-"C" 或作業系
統支援的指令）；請注意，由使用者產生的程式中斷會引發
"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 子句*（"try" 和 "except" 關鍵字之間的陳述式）。

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

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

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

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

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

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

   class B(Exception):
       pass

   class C(B):
       pass

   class D(C):
       pass

   for cls in [B, C, D]:
       try:
           raise cls()
       except D:
           print("D")
       except C:
           print("C")
       except B:
           print("B")

請注意，如果 *except 子句*的順序被反轉（把 "except B" 放到第一個），則
會印出 B、B、B ­­——第一個符合的 *except 子句*會被觸發。

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

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

   >>> try:
   ...     raise Exception('spam', 'eggs')
   ... except Exception as inst:
   ...     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 = spam
   y = eggs

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

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

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

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

   import sys

   try:
       f = open('myfile.txt')
       s = f.readline()
       i = int(s.strip())
   except OSError as err:
       print("OS error:", err)
   except ValueError:
       print("Could not convert data to an integer.")
   except Exception as err:
       print(f"Unexpected {err=}, {type(err)=}")
       raise

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

   for arg in sys.argv[1:]:
       try:
           f = open(arg, 'r')
       except OSError:
           print('cannot open', arg)
       else:
           print(arg, 'has', len(f.readlines()), 'lines')
           f.close()

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

例外的處理者不僅處理 *try 子句*內立即發生的例外，還處理 *try 子句*內（
即使是間接地）呼叫的函式內部發生的例外。例如：

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


8.4. 引發例外
=============

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

   >>> raise NameError('HiThere')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       raise NameError('HiThere')
   NameError: HiThere

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

   raise ValueError  # 'raise ValueError()' 的簡寫

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

   >>> 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 <module>
       raise NameError('HiThere')
   NameError: HiThere


8.5. 例外鏈接 (Exception Chaining)
==================================

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

   >>> try:
   ...     open("database.sqlite")
   ... except OSError:
   ...     raise RuntimeError("unable to handle error")
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, 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>", line 4, in <module>
       raise RuntimeError("unable to handle error")
   RuntimeError: unable to handle error

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

   # exc 必須是例外實例或 None。
   raise RuntimeError from exc

要變換例外時，這種方式很有用。例如：

   >>> def func():
   ...     raise ConnectionError
   ...
   >>> try:
   ...     func()
   ... except ConnectionError as exc:
   ...     raise RuntimeError('Failed to open database') from exc
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       func()
       ~~~~^^
     File "<stdin>", line 2, in func
   ConnectionError

   The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError('Failed to open database') from exc
   RuntimeError: Failed to open database

它也容許使用慣用語 "from None" 來停用自動例外鏈接：

   >>> try:
   ...     open('database.sqlite')
   ... except OSError:
   ...     raise RuntimeError from None
   ...
   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError from None
   RuntimeError

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


8.6. 使用者自定的例外
=====================

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

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

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

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


8.7. 定義清理動作
=================

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

   >>> try:
   ...     raise KeyboardInterrupt
   ... finally:
   ...     print('Goodbye, world!')
   ...
   Goodbye, world!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       raise KeyboardInterrupt
   KeyboardInterrupt

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

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

* 一個例外可能發生於 "except" 或 "else" 子句的執行過程。同樣地，該例外
  會在 "finally" 子句執行後被重新引發。

* 如果 "finally" 子句執行 "break"、"continue" 或 "return" 陳述式，則例
  外不會被重新引發。這可能會令人困惑，因此不建議這麼做。從 3.14 版本開
  始，編譯器會為此發出 "SyntaxWarning"（詳見 **PEP 765**）。

* 如果 "try" 陳述式遇到 "break"、"continue" 或 "return" 陳述式，則
  "finally" 子句會在執行 "break"、"continue" 或 "return" 陳述式之前先
  執行。

* 如果 "finally" 子句中包含 "return" 陳述式，則回傳值會是來自
  "finally" 子句的 "return" 陳述式的回傳值，而不是來自 "try" 子句的
  "return" 陳述式的回傳值。這可能會令人困惑，因此不建議這麼做。從 3.14
  版本開始，編譯器會為此發出 "SyntaxWarning"（詳見 **PEP 765**）。

例如：

   >>> def bool_return():
   ...     try:
   ...         return True
   ...     finally:
   ...         return False
   ...
   >>> bool_return()
   False

另一個比較複雜的範例：

   >>> 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 <module>
       divide("2", "1")
       ~~~~~~^^^^^^^^^^
     File "<stdin>", line 3, in divide
       result = x / y
                ~~^~~
   TypeError: unsupported operand type(s) for /: 'str' and 'str'

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

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


8.8. 預定義的清理動作
=====================

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

   for line in open("myfile.txt"):
       print(line, end="")

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

   with open("myfile.txt") as f:
       for line in f:
           print(line, end="")

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


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

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

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

   >>> def f():
   ...     excs = [OSError('error 1'), SystemError('error 2')]
   ...     raise ExceptionGroup('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()
   ... except Exception as e:
   ...     print(f'caught {type(e)}: {e}')
   ...
   caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)
   >>>

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

   >>> def f():
   ...     raise ExceptionGroup(
   ...         "group1",
   ...         [
   ...             OSError(1),
   ...             SystemError(2),
   ...             ExceptionGroup(
   ...                 "group2",
   ...                 [
   ...                     OSError(3),
   ...                     RecursionError(4)
   ...                 ]
   ...             )
   ...         ]
   ...     )
   ...
   >>> try:
   ...     f()
   ... except* OSError as e:
   ...     print("There were OSErrors")
   ... except* SystemError as e:
   ...     print("There were SystemErrors")
   ...
   There were OSErrors
   There 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 = []
   ... for test in tests:
   ...     try:
   ...         test.run()
   ...     except Exception as e:
   ...         excs.append(e)
   ...
   >>> if excs:
   ...    raise ExceptionGroup("Test Failures", excs)
   ...


8.10. 用註解使例外更詳細
========================

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

   >>> try:
   ...     raise TypeError('bad type')
   ... except Exception as e:
   ...     e.add_note('Add some information')
   ...     e.add_note('Add some more information')
   ...     raise
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       raise TypeError('bad type')
   TypeError: bad type
   Add some information
   Add some more information
   >>>

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

   >>> def f():
   ...     raise OSError('operation failed')
   ...
   >>> excs = []
   >>> for i in range(3):
   ...     try:
   ...         f()
   ...     except Exception as e:
   ...         e.add_note(f'Happened in Iteration {i+1}')
   ...         excs.append(e)
   ...
   >>> raise ExceptionGroup('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
       +------------------------------------
   >>>
