2.26. エラーの送出

関数は、例外を送出することで呼び出し元に問題を知らせることができます。キーワードは raise です:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

square_root(-1) を呼び出すと raise の行で停止し、関数から飛び出して、呼び出し元で適合する except を探します。どの呼び出し元も捕捉しなければ、スクリプトはトレースバックとともに終了します。

2.26.1. 番兵を返す代わりに送出する理由

「不正な入力」を報告する2つの方法:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

通常は例外の形の方が優れています:

  • 呼び出し元は、try を使うか、例外を伝播させるかして、エラーの場合を意図的に処理しなければなりません。番兵は忘れやすく、通常の結果と取り違えやすいものです。

  • エラーメッセージは例外とともに伝わります。番兵の手法では、診断情報をどこか別の場所に付けなければなりません。

  • 未処理の例外に対するデフォルトの動作は、問題のある呼び出しを指し示すトレースバックを伴った派手なクラッシュです。静かな None の返却は、後で見つけにくいバグになります。

番兵を使うのは、「見つからない」ことが例外的でない日常的な結果である場合だけにしましょう。dict.get() がキーが存在しないときに None を返すのは、まさにルックアップが時々ヒットしないことが想定されているからです。

2.26.2. カスタム例外クラス

呼び出し元が組み込みエラーと区別したいかもしれない問題を送出するには、Exception のサブクラスを定義します:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

class の本体が空でも問題ありません。呼び出し元はクラスで捕捉するため、重要なのは名前そのものです。呼び出し元がファミリー全体を1つのブロックで捕捉したい場合は、関連するエラーを共通の基底クラスの下にまとめます。

2.26.2.1. 再送出

except ブロック内の裸の raise は、現在の例外を再送出し、次のハンドラへ伝播させます:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

これは、関数がエラーを実際には処理せずに観測したい(ログに記録する、カウントする、部分的な変更を取り消す)場合の正しい形です。

2.26.3. 捕捉すべき場合と伝播させるべき場合

便利な経験則:

  • 意味のある回復ができるレベルで例外を捕捉してください。デフォルトで置き換える、再試行する、不正な入力をスキップする、などです。

  • クラッシュ以外に有用なことが何もない場合や、回復の方法を知っているのが上の層である場合は、例外を伝播させてください。

呼び出しスタックの途中にある関数がすべてのエラーを飲み込んで静かに返すと、失敗が追跡不能になります。例外は、本当に対処の計画を持つコードに到達するまで伝わせる方を優先してください。