處理 Python 拋出的 Exception 例外異常並使用 Assert 確保程式正常執行

Python 寫久了不免會經常遇到各種錯誤訊息,尤其是各式各樣的例外(Exception)異常:

  • SyntaxError,這個排第一應該當之無愧 XD
  • IndexError、NameError、ValueError
  • UnboundLocalError
    通常是在 Function 裡面使用到了一個後面宣告的變數(例如 x),即使該變數和某 Global 變數同名,若就是想要修改 Global 的那一個也必須先宣告 global x
  • KeyboardInterrupt,例如使用者按了 Ctrl-C

詳細請參考 Python 內建的 Exception 列表。發生 Exception 的當下往往伴隨著反向追蹤 Traceback 程式碼指出問題所在,省下不少工程師們的時間。

Exception Handling

不過還是會遇到不得不好好面對 Exception 的時候,例如希望將字串轉為數字:

def str_to_int(text):
    return int(text)

但如果輸入的 text 不是 10 進位的數字的話程式就會拋出 ValueError 例外,此時初學者可能會 try…except… 這樣寫:

def str_to_int(text):
    try:
        return int(text) # NOTICE: syntax error here won't get error
    except:
        return None

此範例很簡單所以不容易出問題,然而有些 Exception 的行為只是單純 Bug,如果無謂使用前面的做法就會錯過這些問題。

Recommended Syntax

因此較推薦的寫法是只抓取自己預期的例外情況:

def str_to_int(text):
    try:
        # when code here is complex, you may not want to catch all exceptions.
        # that way you can find out bugs in a early stage of development
        return int(text)
    except ValueError:
        return None

另外如果想要把例外狀況印出來可以設為一個變數使用:

def str_to_int(text):
    try:
        return int(text)
    except ValueError as e:
        print(e)
        return None

Advanced Syntax

而如果需要抓多種例外狀況可以並列寫:

def str_to_int(text):
    try:
        return int(text)
    except ValueError as e:
        print(e)
        return None
    except NameError as e:
        print(e)
        return None

然後你發現發生例外狀況之後還是有些共通的程式要處理,於是可以加入 finally Block:

def str_to_int(text):
    try:
        return int(text)
    except ValueError as e:
        print(e)
    except NameError as e:
        print(e)
    finally:
        return None

以上範例雖然有點殺雞用牛刀,但希望大家可以著重在 try…except… 寫法的部分。

User-Defined Exceptions

在撰寫自己的程式時,如果有條件需要跑出例外異常,建議的做法是自己定義一個新的 Exception 以免跟內建的例外異常混淆,並且還能針對自己客製化的例外做處理。

class MyError(Exception):
    """ raise anytime I want to """

def str_to_int(text):
    if text == "sakyouz": # just a condition shall raise an user-defined exception
        raise MyError
        # raise MyError("if you want to customize error message")

    try:
        return int(text)
    except ValueError:
        return None

try:
    # this actually raise NameError because x is not defined,
    # and since we have a good practice, Python tells us such problem.
    str_to_int(x)
except MyError:
    pass # you should do serious error handling here or at least logging

Suppress Exception

但如果不在意某個 Exception 的發生又覺得每次都要寫 try…except… 很麻煩,Python 內建了一個 Module 幫你:

from contextlib import suppress

with suppress(MyError):
    x = str_to_int("sakyouz") # some codes may raise an error that we don't care

Assertion Handling

那 Assert 在幹嘛?Assert 不用處理嗎?還真不用,相信你也從來不會看到 Python 告訴你發生 Assert。

Assert 的條件是用來確保程式內部正確的運行,無關乎使用者也完全不預期會發生,如果遇到了 Assert 就必須視為 Bug。

承前面的例子,可以卡一個型別檢查的 Assert:

def str_to_int(text):
    assert isinstance(text, str) # subclass of str also pass for isinstance 
    try:
        return int(text)
    except ValueError:
        return None

如果要加入 Assert 訊息,就加逗點然後寫在後面:

def str_to_int(text):
    assert isinstance(text, str), f"{typeof(text)=} shall be str or subclass of str."
    try:
        return int(text)
    except ValueError:
        return None

References