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 子句中的例外名稱,則將其傳遞到外層的
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 子句會被觸發。
最後一個 except 子句可以省略例外名稱,以統一處理所有其他例外。但使用上要極其小心,因為這種方式容易遮蔽真正的程式設計錯誤!它也可用於印出錯誤訊息,然後重新引發例外(也讓呼叫者可以處理該例外):
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:
print("Unexpected error:", sys.exc_info()[0])
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
陳述式保護的程式碼所引發的例外。
當例外發生時,它可能有一個相關的值,也就是例外的引數。此引數的存在與否及它的類型,是取決於例外的類型。
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)¶
raise
陳述式容許一個選擇性的 from
,它透過被引發例外中的 __cause__
屬性的設定,來啟用例外鏈接。例如:
# exc must be exception instance or None.
raise RuntimeError from exc
要變換例外時,這種方式很有用。例如:
>>> def func():
... raise IOError
...
>>> try:
... func()
... except IOError 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
OSError
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
當例外是在一個 except
或 finally
段落的內部被引發時,例外鏈接會自動發生。要禁止例外鏈接,可以使用慣用語 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 衍生出來。
Exception classes can be defined which do anything any other class can do, but are usually kept simple, often only offering a number of attributes that allow information about the error to be extracted by handlers for the exception.
大多數的例外定義,都會以「Error」作為名稱結尾,類似於標準例外的命名。
許多標準模組會定義它們自己的例外,以報告在其定義的函式中發生的錯誤。更多有關 class 的資訊,詳見Class(類別)章節。
8.7. 定義清理動作¶
try
陳述式有另一個選擇性子句,用於定義在所有情況下都必須被執行的清理動作。例如:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
如果 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 總是會被關閉。和檔案一樣,提供預定義清理動作的物件會在說明文件中表明這一點。