Python 1.5 中的標準異常類
Python 1.5 中的標準異常類
(已更新至 Python 1.5.2 -baw)使用者定義的 Python 異常可以是字串或 Python 類。由於類在用作異常時具有許多優良特性,因此期望遷移到僅使用類的情況。在 Python 1.5 alpha 4 之前,Python 的標準異常(IOError、TypeError 等)被定義為字串。將這些異常更改為類帶來了一些特別棘手的向後相容性問題。
在 Python 1.5 及更高版本中,標準異常是 Python 類,並且添加了一些新的標準異常。廢棄的 AccessError 異常已被刪除。由於此更改可能會(儘管不太可能)破壞現有程式碼,因此可以使用命令列選項 -X 呼叫 Python 直譯器以停用此功能,並像以前一樣使用字串異常。此選項是臨時措施——最終基於字串的標準異常將從語言中完全刪除。Python 2.0 中是否允許使用者定義的字串異常尚未決定。
標準異常層次結構
請看標準異常層次結構。它在新的標準庫模組 exceptions.py 中定義。Python 1.5 以來新增的異常標有 (*)。
Exception(*)
|
+-- SystemExit
+-- StandardError(*)
|
+-- KeyboardInterrupt
+-- ImportError
+-- EnvironmentError(*)
| |
| +-- IOError
| +-- OSError(*)
|
+-- EOFError
+-- RuntimeError
| |
| +-- NotImplementedError(*)
|
+-- NameError
+-- AttributeError
+-- SyntaxError
+-- TypeError
+-- AssertionError
+-- LookupError(*)
| |
| +-- IndexError
| +-- KeyError
|
+-- ArithmeticError(*)
| |
| +-- OverflowError
| +-- ZeroDivisionError
| +-- FloatingPointError
|
+-- ValueError
+-- SystemError
+-- MemoryError
所有異常的根類是新異常 Exception。由此派生出另外兩個類:StandardError,它是所有標準異常的根類;以及 SystemExit。建議新程式碼中使用者定義的異常派生自 Exception,儘管出於向後相容性原因,這不是必需的。最終此規則將收緊。
SystemExit 派生自 Exception,因為它雖然是異常,但它不是錯誤。
大多數標準異常都是 StandardError 的直接後代。一些相關的異常使用從 StandardError 派生的中間類進行分組;這使得可以在一個 except 子句中捕獲多個不同的異常,而無需使用元組表示法。
我們曾考慮引入更多相關的異常組,但未能決定最佳分組方式。在像 Python 這樣動態的語言中,很難說 TypeError 是“程式錯誤”、“執行時錯誤”還是“環境錯誤”,所以我們決定不予決定。可以說 NameError 和 AttributeError 應該派生自 LookupError,但這是有爭議的,並且完全取決於應用程式。
異常類定義
標準異常的 Python 類定義是從標準模組“exceptions”匯入的。您無法透過修改此檔案來使其更改自動顯示在標準異常中;內建模組期望 exceptions.py 中定義的當前層次結構。
有關標準異常類的詳細資訊可在 Python 庫參考手冊中關於 exceptions 模組的條目中找到。
對 raise 的更改
raise 語句已擴充套件,允許在不顯式例項化的情況下引發類異常。允許以下形式,稱為 raise 語句的“相容性形式”:
raise異常raise異常, 引數raise異常, (引數, 引數, ...)
當 異常 是一個類時,這些等同於以下形式:
raise異常()raise異常(引數)raise異常(引數, 引數, ...)
請注意,這些都是以下形式的示例
raise例項
這本身就是以下形式的簡寫:
raise類, 例項
其中 類 是 例項 所屬的類。在 Python 1.4 中,只允許以下形式:
raise類, 例項 和raise例項
在 Python 1.5(從 1.5a1 開始)中,添加了以下形式:
raise類 和raise類, 引數(s)
字串異常的允許形式保持不變。
出於各種原因,將 None 作為 raise 的第二個引數傳遞等同於省略它。特別是,語句
raise類,None
等同於
raise類()
而不是
raise類(None)
同樣,語句
raise類, 值
其中 值 恰好是一個元組,等同於將元組的項作為單個引數傳遞給類建構函式,而不是將 值 作為單個引數傳遞(空元組呼叫不帶引數的建構函式)。這會產生差異,因為 f(a, b) 和 f((a, b)) 之間存在差異。
這些都是折衷方案——它們與標準異常通常採用的引數型別(例如簡單字串)配合得很好。為了在新程式碼中清晰明瞭,建議採用以下形式:
raise類(引數, ...)
(即,顯式呼叫建構函式)。
這有什麼幫助?
引入相容性形式的動機是為了允許向後相容引發標準異常的舊程式碼。例如,一個 __getattr__ 鉤子可能會在所需屬性未定義時呼叫以下語句:
raise AttributeError, attrname
使用新的類異常,應引發的正確異常是 AttributeError(attrname);相容性形式確保舊程式碼不會中斷。(事實上,希望與 -X 選項相容的新程式碼 必須 使用相容性形式,但強烈不鼓勵這樣做。)
(事實上,希望與 -X 選項相容的新程式碼 必須 使用相容性形式,但強烈不鼓勵這樣做。)
對 except 的更改
對 try 語句的 except 子句沒有進行任何使用者可見的更改。
在內部,發生了許多變化。例如,從 C 語言引發的類異常在捕獲時例項化,而不是在引發時例項化。這是一種效能最佳化,以便完全在 C 語言中引發和捕獲的異常無需承擔例項化成本。例如,在 for 語句中遍歷列表時,列表物件會在列表末尾引發 IndexError,但該異常在 C 語言中被捕獲,因此從未例項化。
可能會出現什麼問題?
新設計盡力不破壞舊程式碼,但在某些情況下,為了避免破壞程式碼,不值得妥協新語義。換句話說,一些舊程式碼可能會被破壞。這就是為什麼有 -X 開關;然而,這不應該成為不修復程式碼的藉口。
有兩種型別的破壞:有時,當代碼捕獲類異常但期望字串異常時,它會列印一些略微奇怪的錯誤訊息。有時,但頻率低得多,程式碼實際上會在其錯誤處理中崩潰或以其他方式做錯事。
非致命性破壞
第一種破壞的例子是試圖列印異常名稱的程式碼,例如:
try:
1/0
except:
print "Sorry:", sys.exc_type, ":", sys.exc_value
對於基於字串的異常,這會列印類似:對於基於類的異常,它將列印:Sorry: ZeroDivisionError : integer division or modulo
奇怪的Sorry: exceptions.ZeroDivisionError : integer division or modulo
exceptions.ZeroDivisionError 出現是因為當異常型別是類時,它被列印為 模組名.類名。這由 Python 內部處理。致命性破壞
更嚴重的是破壞錯誤處理程式碼。這通常發生是因為錯誤處理程式碼期望異常或與異常關聯的值具有特定型別(通常是字串或元組)。在新方案中,型別是一個類,值是一個類例項。例如,以下程式碼會中斷:
try:
raise Exception()
except:
print "Sorry:", sys.exc_type + ":", sys.exc_value
因為它試圖將異常型別(一個類物件)與字串連線。一個修復方法(也適用於前面的示例)是這樣寫:
try:
raise Exception()
except:
etype = sys.exc_type # Save it; try-except overwrites it!
try:
ename = etype.__name__ # Get class name if it is a class
except AttributeError:
ename = etype
print "Sorry:", str(ename) + ":", sys.exc_value
請注意這個例子如何避免了顯式型別測試!相反,它只是捕獲了當 __name__ 屬性未找到時引發的(新)異常。為了絕對確保我們正在連線字串,應用了內建函式 str()。另一個例子涉及對與異常關聯的值型別假設過多的程式碼。例如:
try:
open('file-doesnt-exist')
except IOError, v:
if type(v) == type(()) and len(v) == 2:
(code, message) = v
else:
code = 0
message = v
print "I/O Error: " + message + " (" + str(code) + ")"
print
這段程式碼知道 IOError 通常會引發一個 (errorcode, message) 形式的元組,有時也只引發一個字串。然而,由於它顯式地測試值的元組性質,當值是一個例項時,它會崩潰!同樣,補救措施是直接嘗試解包元組,如果失敗,則使用備用策略:
try:
open('file-doesnt-exist')
except IOError, v:
try:
(code, message) = v
except:
code = 0
message = v
print "I/O Error: " + str(message) + " (" + str(code) + ")"
print
這之所以有效,是因為元組解包語義已經放寬,可以與右側的任何序列一起使用(參見下面的“序列解包”部分),並且標準異常類可以像序列一樣訪問(透過它們的 __getitem__ 方法,參見上文)。請注意,第二個 try-except 語句沒有指定要捕獲的異常——這是因為對於字串異常,引發的異常是“TypeError: unpack non-tuple”,而對於類異常,它是“ValueError: unpack sequence of wrong size”。這是因為字串是一個序列;我們必須假設錯誤訊息總是超過兩個字元長!
(另一種方法是使用 try-except 來測試 errno 屬性是否存在;將來,這將是有意義的,但目前為了與字串異常相容,需要更多的程式碼。)
C API 的變更
XXX 待詳細描述
int PyErr_ExceptionMatches(PyObject *); int PyErr_GivenExceptionMatches(PyObject *, PyObject *); void PyErr_NormalizeException(PyObject**, PyObject**, PyObject**);
應優先使用 PyErr_ExceptionMatches(exception) 而不是 PyErr_Occurred()==exception,因為當引發的異常是所測試異常的派生類時,後者將返回不正確的結果。
PyErr_GivenExceptionMatches(raised_exception, exception) 執行與 PyErr_ExceptionMatches() 相同的測試,但允許您顯式傳遞引發的異常。
PyErr_NormalizeException() 主要用於內部使用。
其他變更
作為同一專案的一部分,語言進行了一些更改。
新的內建函式
引入了兩個用於類測試的新內建函式(因為該功能必須在 C API 中實現,所以沒有理由不讓 Python 程式設計師訪問它)。
issubclass(D, C) 當且僅當類 D 直接或間接派生自類 C 時返回 true。issubclass(C, C) 始終返回 true。兩個引數都必須是類物件。
isinstance(x, C) 當且僅當 x 是 C 的例項或 C 的(直接或間接)子類的例項時返回 true。第一個引數可以是任何型別;如果 x 不是任何類的例項,isinstance(x, C) 始終返回 false。第二個引數必須是類物件。
序列解包
以前的 Python 版本要求“解包”賦值的左側和右側之間進行精確的型別匹配,例如:
要求 x 是一個包含三個項的元組,而(a, b, c) = x
要求 x 是一個包含三個項的列表。[a, b, c] = x
作為同一專案的一部分,兩個語句的右側可以是任何具有正好三個項的序列。這使得可以以向後相容的方式從 IOError 異常中提取例如 errno 和 strerror 值:
try:
f = open(filename, mode)
except IOError, what:
(errno, strerror) = what
print "Error number", errno, "(%s)" % strerror
同樣的方法也適用於 SyntaxError 異常,但有一個前提條件是 info 部分並非總是存在:
try:
c = compile(source, filename, "exec")
except SyntaxError, what:
try:
message, info = what
except:
message, info = what, None
if info:
"...print source code info..."
print "SyntaxError:", msg
