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
陳述式運作方式如下。
如果沒有發生例外,則 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
子句執行後被重新引發。如果
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 總是會被關閉。和檔案一樣,提供預定義清理動作的物件會在說明文件中表明這一點。