8. 錯誤和異常

到目前為止,錯誤訊息只是被提及,但如果你嘗試過這些示例,你可能已經看到了一些。 錯誤(至少)有兩種明顯的型別:語法錯誤異常

8.1. 語法錯誤

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

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

解析器重複出錯的行,並顯示小箭頭,指向在該行中檢測到錯誤的標記。 該錯誤可能是由指示的標記之前缺少標記引起的。 在示例中,錯誤是在函式 print() 處檢測到的,因為它之前缺少冒號 (':')。 列印檔名和行號,以便你知道在輸入來自指令碼時應該在哪裡查詢。

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 子句更好,因為它避免了意外捕獲未被 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 的唯一引數表示要引發的異常。這必須是異常例項或異常類(派生自 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 語句,則不會重新引發異常。

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

  • 如果 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>
    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
    +------------------------------------
>>>