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
錯誤訊息的最後一行指示發生了什麼。 異常有不同的型別,並且型別作為訊息的一部分列印:示例中的型別是 ZeroDivisionError
、 NameError
和 TypeError
。 作為異常型別列印的字串是發生的內建異常的名稱。 這對於所有內建異常都是如此,但對於使用者定義的異常可能不適用(儘管這是一個有用的約定)。 標準異常名稱是內建識別符號(而不是保留關鍵字)。
該行的其餘部分根據異常的型別和導致異常的原因提供了詳細資訊。
錯誤訊息的前面部分以堆疊回溯的形式顯示異常發生的上下文。 通常,它包含列出原始碼行的堆疊回溯; 但是,它不會顯示從標準輸入讀取的行。
內建異常 列出了內建異常及其含義。
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
語句的工作方式如下。
如果未發生異常,則跳過 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
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
的唯一引數表示要引發的異常。這必須是異常例項或異常類(派生自 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
子句後重新引發該異常。在執行
except
或else
子句期間可能會發生異常。同樣,在執行finally
子句後重新引發該異常。如果
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>
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.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
+------------------------------------
>>>