# 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) 會重複犯錯的那一行，並用一個小「箭頭」指向該行檢測到的第一個錯誤點。錯誤是由箭頭之前的標記 (token) 導致的（或至少是在這裡檢測到的）：此例中，錯誤是在 `print()` 函式中被檢測到，因為在它前面少了一個冒號（`':'`）。檔案名稱和行號會被印出來，所以如果訊息是來自腳本時，就可以知道去哪裡找問題。

## 8.2. 例外 (Exception)

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

    >>> 10 * (1/0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ZeroDivisionError: division by zero
    >>> 4 + spam*3
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'spam' is not defined
    >>> '2' + 2
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    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 子句可以用一組括號內的 tuple 列舉多個例外，例如：

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

一個在 `except` 子句中的 class（類別）和一個例外是可相容的，只要它與例外是同一個 class 或是為其 base class（基底類別）；反之則無法成立——列出 derived class （衍生類別）的 except 子句並不能與 base class 相容。例如，以下程式碼會依序印出 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 子句會被觸發。

所有例外繼承自 `BaseException`，以統一處理所有其他例外，但使用上要極其小心，因為這種方式容易遮蔽真正的程式設計錯誤！它也可用於印出錯誤訊息，然後重新引發例外（也讓呼叫者可以處理該例外）：

    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 BaseException as err:
        print(f"Unexpected {err=}, {type(err)=}")
        raise

或者讓最後一個 except 子句可以省略例外名稱，但之後例外的值必須是從 `sys.exc_info()[1]` 得到的。

`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` 陳述式保護的程式碼所引發的例外。

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

except 子句可以在例外名稱後面指定一個變數。這個變數被綁定到一個例外實例 (instance)，其引數儲存在 `instance.args` 中。為了方便，例外實例會定義 `__str__()`，因此引數可以直接被印出而無須引用 `.args`。你也可以在引發例外前就先建立一個例外實例，並隨心所欲地為它加入任何屬性。

    >>> 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

如果一個例外有引數，則它們會被印在未處理例外的訊息的最後一部分（「細節」）。

例外的處理者不僅處理 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>
    NameError: HiThere

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

    raise ValueError  # shorthand for '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>
    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>
    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>
    RuntimeError: unable to handle error

为了表明一个异常是另一个异常的直接后果， `raise` 语句允许一个可选的 `from` 子句:

    # exc must be exception instance or 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>
      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>
    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>
    RuntimeError

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

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

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

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

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

許多標準模組會定義它們自己的例外，以報告在其定義的函式中發生的錯誤。更多有關 class 的資訊，詳見Class（類別）章節。

## 8.7. 定義清理動作

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

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

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

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

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

- 如果 `finally` 子句執行 `break`、`continue` 或 `return` 陳述式，則例外不會被重新引發。

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

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

例如：

    >>> 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>
      File "<stdin>", line 3, in divide
    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 總是會被關閉。和檔案一樣，提供預定義清理動作的物件會在說明文件中表明這一點。