例外繼承架構

April 29, 2022

在使用多個 except 時,必須留意例外繼承架構。

多個 except

如果例外在 except 的比對過程中,就符合了某個父型態,後續即使定義了 except 比對子型態例外,也等同於沒有定義。例如:

try:
    dividend = int(input('輸入被除數:'))
    divisor = int(input('輸入除數:'))
    print(f'{dividend} / {divisor} = {dividend / divisor}')
except ArithmeticError:
    print('運算錯誤')
except ZeroDivisionError:
    print('除零錯誤')

執行上面這個程式片段,永遠不會看到 '除零錯誤' 的訊息,因為在例外繼承架構中,ArithmeticErrorZeroDivisionError 的父類別,發生 ZeroDivisionError 時,except 比對會先遇到 ArithmeticError,就語義上 ZeroDivisionError 是一種 ArithmeticError,因此就執行了對應的區塊,後續的 except 就不會再比對了。

在 Python 中,例外都是 BaseException 的子類別,當使用 except 沒有指定例外型態時,就是比對 BaseException。例如:

while True:
    try:
        print('跑跑跑...')
    except:
         print('Shit happens!')

上面這個程式,無法透過 Ctrl+C 來中斷迴圈,因為只寫了 except 而沒有指定例外型態,這等同於比對了 BaseException,也就是全部的例外都會比對成功,這包括了 KeyboardInterrupt 例外,執行過 except 區塊後,又仍在迴圈之中,因此永不停止。

然而,如果在 except 指定了 Exception 型態,就可以透過 Ctrl+C 中斷程式。例如:

while True:
    try:
        print('跑跑跑...')
    except Exception:
         print('Shit happens!')

KeyboardInterrupt 例外不是 Exception 的子類別,因此沒有對應的 except 可以處理 KeyboardInterrupt 例外,迴圈流程會被中斷,最後整個程式結束。

內建例外架構

Python 標準程式庫中,完整的例外繼承架構,可以在官方文件〈Built-in Exceptions〉中找到,基於查閱方便,底下直接列出。

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

先前談過,Python 中的例外並非都是錯誤,有時代表著一種通知,例如,StopIteration 只是通知迭代流程無法再進行了;方才看到的 KeyboardInterrupt 也是,表示發生了一個鍵盤中斷;SystemExit 是由 sys.exit 引發的例外,表示離開 Python 程式;GeneratorExit 會在產生器的 close 方法被呼叫時,從當時暫停的位置引發,如果在定義產生器時,想在 close 時為產生器做些資源善後等動作,就可以使用。例如:

>>> def natural():
...     n = 0
...     try:
...         while True:
...             n += 1
...             yield n
...     except GeneratorExit:
...          print('GeneratorExit', n)
...
>>> n = natural()
>>> next(n)
1
>>> n.close()
GeneratorExit 1
>>>

SystemExitKeyboardInterruptGeneratorExit 都直接繼承了 BaseException,這是因為它們在 Python 中,都是屬於退出系統的例外,如果想自訂例外,不要直接繼承 BaseException,而應該繼承 ExceptionException 的相關子類別。

在繼承 Exception 自定義例外時,如果自定義了 __init__,建議將自定義的 __init__ 傳入的引數,透過 super().__init__(arg1, arg2, …) 來呼叫 Exception__init__,因為 Exception__init__ 預設接受所有傳入的引數,而這些被接受的全部引數,可透過 args 屬性取得。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter