8. 錯誤和異常

到目前為止,我們僅僅提到了錯誤訊息,但是如果你嘗試過這些示例,你可能已經遇到過一些。錯誤至少有兩種可區分的型別:語法錯誤異常

8.1. 語法錯誤

語法錯誤,也稱為解析錯誤,可能是你還在學習 Python 時遇到的最常見的抱怨。

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

解析器會重複顯示出錯的行,並用小箭頭指向檢測到錯誤的位置。請注意,這不總是需要修復的地方。在這個例子中,錯誤是在 print() 函式處檢測到的,因為它前面缺少一個冒號(':')。

檔名稱(我們示例中的 <stdin>)和行號會被打印出來,這樣如果輸入來自檔案,你就知道在哪裡查詢。

8.2. 異常

即使語句或表示式在語法上是正確的,但當嘗試執行它時,也可能會導致錯誤。在執行期間檢測到的錯誤稱為 異常,它們並非無條件致命:你很快就會學到如何在 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

錯誤訊息的最後一行指示發生了什麼。異常有不同的型別,並且型別作為訊息的一部分打印出來:示例中的型別是 ZeroDivisionErrorNameErrorTypeError。作為異常型別列印的字串是發生的內建異常的名稱。所有內建異常都是如此,但對於使用者定義的異常則不一定(儘管這是一個有用的約定)。標準異常名稱是內建識別符號(不是保留關鍵字)。

行的其餘部分根據異常型別和導致異常的原因提供詳細資訊。

錯誤訊息的前一部分以堆疊回溯的形式顯示了異常發生的上下文。通常,它包含列出源行的堆疊回溯;但是,它不會顯示從標準輸入讀取的行。

內建異常 列出了內建異常及其含義。

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 子句tryexcept 關鍵字之間的語句)。

  • 如果沒有發生異常,則跳過 except 子句try 語句的執行完成。

  • 如果在執行 try 子句期間發生異常,則跳過子句的其餘部分。然後,如果其型別與 except 關鍵字後指定的異常匹配,則執行 except 子句,然後執行在 try/except 塊之後繼續。

  • 如果發生的異常與 except 子句 中指定的異常不匹配,則它會被傳遞給外部的 try 語句;如果沒有找到處理程式,則它是一個 未處理的異常,執行會以錯誤訊息停止。

一個 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 子句 可以在異常名稱後指定一個變數。該變數繫結到異常例項,該例項通常有一個 args 屬性,用於儲存引數。為方便起見,內建異常型別定義了 __str__() 以列印所有引數,而無需顯式訪問 .args

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     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

對於未處理的異常,異常的 __str__() 輸出將作為訊息的最後一部分(“詳細資訊”)列印。

BaseException 是所有異常的公共基類。它的一個子類 Exception 是所有非致命異常的基類。不屬於 Exception 子類的異常通常不會被處理,因為它們用於指示程式應該終止。它們包括 SystemExit(由 sys.exit() 觸發)和 KeyboardInterrupt(當用戶希望中斷程式時觸發)。

Exception 可以用作捕獲(幾乎)所有內容的萬用字元。然而,處理異常時儘可能具體,並讓任何意外的異常繼續傳播是一個好習慣。

處理 Exception 最常見的模式是列印或記錄異常,然後重新引發它(也允許呼叫者處理異常)

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

tryexcept 語句有一個可選的 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 子句中新增額外程式碼更好,因為它避免了意外捕獲並非由受 tryexcept 語句保護的程式碼引發的異常。

異常處理程式不僅處理在 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 的唯一引數指示要引發的異常。這必須是異常例項或異常類(一個派生自 BaseException 的類,例如 Exception 或其子類)。如果傳遞的是異常類,它將透過呼叫其不帶引數的建構函式來隱式例項化。

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

8.5. 異常鏈

如果在 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 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>
    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. 使用者自定義異常

程式可以透過建立新的異常類來命名自己的異常(有關 Python 類的更多資訊,請參閱 )。異常通常應該直接或間接派生自 Exception 類。

異常類可以像任何其他類一樣定義,但通常保持簡單,通常只提供一些屬性,允許異常的處理程式提取有關錯誤的資訊。

大多數異常的命名都以“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 語句完成之前的最後任務執行。finally 子句無論 try 語句是否產生異常都會執行。以下幾點討論了異常發生時更復雜的情況

  • 如果在執行 try 子句期間發生異常,該異常可能由 except 子句處理。如果異常未由 except 子句處理,則在 finally 子句執行後重新引發異常。

  • 異常可能發生在執行 exceptelse 子句期間。同樣,在 finally 子句執行後重新引發異常。

  • 如果 finally 子句執行 breakcontinuereturn 語句,則不會重新引發異常。這可能會令人困惑,因此不鼓勵這樣做。從 3.14 版本開始,編譯器會為此發出 SyntaxWarning(請參閱 PEP 765)。

  • 如果 try 語句到達 breakcontinuereturn 語句,則 finally 子句將在 breakcontinuereturn 語句執行之前執行。

  • 如果 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. 引發和處理多個不相關的異常

有些情況下需要報告多個已發生的異常。這在併發框架中經常發生,當多個任務可能並行失敗時,但也有其他用例希望繼續執行並收集多個錯誤,而不是引發第一個異常。

內建的 ExceptionGroup 將一個異常例項列表包裝起來,以便它們可以一起被引發。它本身就是一個異常,因此可以像任何其他異常一樣被捕獲。

>>> 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'>: e
>>>

透過使用 except* 而不是 except,我們可以選擇性地只處理組中與特定型別匹配的異常。在以下示例中,該示例顯示了一個巢狀異常組,每個 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),它接受一個字串並將其新增到異常的註釋列表中。標準的回溯渲染包括所有註釋,按照它們新增的順序,在異常之後。

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