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 異常已被刪除。由於這種更改可能(儘管不太可能)破壞現有程式碼,因此可以呼叫 Python 直譯器的命令列選項 -X
來停用此功能,並像以前一樣使用字串異常。此選項是一項臨時措施 - 最終將從該語言中完全刪除基於字串的標準異常。尚未決定在 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
exceptionraise
exception, argumentraise
exception, (argument, argument, ...)
當 exception 是一個類時,這些等效於以下形式
raise
exception()raise
exception(argument)raise
exception(argument, argument, ...)
請注意,這些都是以下形式的示例
raise
instance
它本身是以下形式的簡寫
raise
class, instance
其中 class 是 instance 所屬的類。在 Python 1.4 中,只允許以下形式
raise
class, instance 和raise
instance
在 Python 1.5(從 1.5a1 開始)中,添加了以下形式
raise
class 和raise
class, argument(s)
字串異常的允許形式保持不變。
由於各種原因,將 None 作為 raise 的第二個引數傳遞等同於省略它。特別是,語句
raise
class,None
等效於
raise
class()
而不是
raise
class(None
)
同樣,語句
raise
class, value
其中 value 恰好是一個元組,等效於將元組的項作為單獨的引數傳遞給類建構函式,而不是將 value 作為單個引數傳遞(空元組呼叫建構函式時不帶引數)。這會產生差異,因為 f(a, b)
和 f((a, b))
之間存在差異。
這些都是妥協 - 它們很好地適用於標準異常通常採用的引數型別(如簡單字串)。為了在新程式碼中清晰起見,建議使用以下形式
raise
class(argument, ...)
(即顯式呼叫建構函式)。
這有什麼幫助?
引入相容性形式的動機是為了允許與引發標準異常的舊程式碼向後相容。例如,__getattr__ 鉤子可能會呼叫語句
raise AttributeError
, attrname
當未定義所需的屬性時。
使用新的類異常,要引發的正確異常將是 AttributeError
(attrname);相容性形式確保舊程式碼不會中斷。(事實上,想要與 -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
,是因為當異常型別是一個類時,它會列印為 modulename.classname。這由 Python 內部處理。致命性中斷
更嚴重的是破壞錯誤處理程式碼。這通常是因為錯誤處理程式碼期望異常或與異常關聯的值具有特定型別(通常是字串或元組)。使用新方案,該型別是一個類,該值是一個類例項。例如,以下程式碼將會中斷
因為它嘗試將異常型別(一個類物件)與字串連線起來。修復(也適用於之前的示例)將是編寫try: raise Exception() except: print "Sorry:", sys.exc_type + ":", sys.exc_value
請注意此示例如何避免顯式型別測試!相反,它只是捕獲當找不到 __name__ 屬性時引發的(新)異常。為了絕對確定我們正在連線一個字串,應用了內建函式 str()。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
另一個示例涉及對與異常關聯的值的型別假設過多的程式碼。例如
此程式碼瞭解 IOError 通常會使用 (errorcode, message) 形式的元組引發,有時只使用字串。但是,由於它顯式測試該值的元組性,因此當該值為例項時它會崩潰!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
同樣,補救方法是繼續並嘗試元組解包,如果失敗,則使用後備策略
這之所以有效,是因為元組解包語義已經放寬,以使用右側的任何序列(請參閱下面的“序列解包”部分),並且可以透過其 __getitem__ 方法(如上所示)像序列一樣訪問標準異常類。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
請注意,第二個 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)
返回 true 當且僅當類 D 是類 C 的直接或間接派生類。issubclass(C, C)
始終返回 true。兩個引數都必須是類物件。
isinstance(x, C)
返回 true 當且僅當 x 是 C 的例項或 C 的(直接或間接)子類的例項。第一個引數可以是任何型別;如果 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