17.

Python の例外処理|try/except/else/finally・raise・独自例外

編集
この記事の要点
  • 構文は try / except / else / finallycatch ではなく except
  • except: だけの裸の書き方は禁止Ctrl + C まで飲み込む。最低でも except Exception
  • 捕まえる範囲は狭く。握りつぶさず、対処できない例外はそのまま上へ投げる
  • ログには logging.exception()。トレースバックごと残る
  • 再送出は raise 単独。原因を付け替えるときは raise 新例外 from e

基本形

try:
    value = int(input("数値: "))
except ValueError as e:
    print(f"数値ではありません: {e}")
else:
    print(f"入力は {value} です")      # 例外が出なかったときだけ
finally:
    print("必ず実行される")            # 成功でも失敗でも通る
実行されるタイミング
try監視したい処理を置く。できるだけ短くする
except指定した例外が起きたとき
else例外が起きなかったとき
finally例外の有無・return の有無に関わらず必ず

複数の例外を捕まえる

try:
    result = data["key"] / divisor
except KeyError:
    result = 0                       # 種類ごとに処理を変える
except ZeroDivisionError:
    result = None
except (TypeError, ValueError) as e: # まとめて扱うときはタプル
    print(f"型か値の問題: {e}")
except Exception as e:               # 最後に広いものを置く
    print(f"想定外: {e}")
    raise                            # 対処できないので上へ投げ直す

狭い例外を先に、広い例外を後に書きます。except Exception を先頭に置くと以降の節に到達しません。

やってはいけない書き方

# 1. 裸の except
try:
    do_something()
except:                # KeyboardInterrupt / SystemExit まで捕まえる
    pass               # Ctrl + C でプログラムを止められなくなる

# 2. 握りつぶし
try:
    save()
except Exception:
    pass               # 失敗が誰にも伝わらず、原因が永久に分からない

# 3. try の範囲が広すぎる
try:
    conn = connect()
    data = fetch(conn)
    parsed = parse(data)
    save(parsed)
except Exception:
    print("失敗しました")    # どこで失敗したか分からない

ログを残さない except: pass は、後から原因を追えなくする最悪のパターンです。握りつぶすなら、なぜ無視してよいのかをコメントで残します。

正しい捕まえ方

import logging

logger = logging.getLogger(__name__)

def load_config(path):
    try:
        with open(path, encoding="utf-8") as f:   # 監視するのはこの 1 行だけ
            text = f.read()
    except FileNotFoundError:
        logger.warning("設定ファイルが無いので既定値を使います: %s", path)
        return {}
    except OSError:
        logger.exception("設定ファイルを読めませんでした")   # トレースバック付き
        raise                                     # 呼び出し側に判断を委ねる

    return parse(text)

logger.exception()except の中でだけ使え、スタックトレースを自動で付けて ERROR で出力します。logger.error(str(e)) だと発生箇所が失われます。

例外を投げる

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError(f"金額は正の数にしてください: {amount}")
    if amount > balance:
        raise ValueError("残高が不足しています")
    return balance - amount

# 原因を保ったまま別の例外に変換する
try:
    config = load()
except OSError as e:
    raise RuntimeError("起動に失敗しました") from e   # 元の例外が __cause__ に残る

from e を付けると、表示に The above exception was the direct cause of... が出て元の原因まで辿れます。付け忘れると根本原因が消えます。

独自例外

class AppError(Exception):
    """このアプリの例外の基底クラス"""

class NotFoundError(AppError):
    def __init__(self, resource_id):
        super().__init__(f"{resource_id} が見つかりません")
        self.resource_id = resource_id      # 後から使える情報を持たせる

class ValidationError(AppError):
    pass

try:
    raise NotFoundError("user-42")
except AppError as e:                       # 自作例外だけまとめて捕まえられる
    print(e)                                # user-42 が見つかりません

アプリ共通の基底クラスを 1 つ作ると、「自分たちが投げた例外」と「ライブラリが投げた例外」を区別できます。継承元は BaseException ではなく Exception にしてください。

finally と with

# finally で後始末
f = open("data.txt", encoding="utf-8")
try:
    print(f.read())
finally:
    f.close()

# with なら閉じ忘れない(推奨)
with open("data.txt", encoding="utf-8") as f:
    print(f.read())

# finally の中の return は例外を消してしまう
def bad():
    try:
        raise ValueError("元の例外")
    finally:
        return "戻り値"      # 例外が握りつぶされる。書かないこと

先に聞くか、後で謝るか

Python には、条件を確認してから実行する書き方(LBYL)と、まずやってみて失敗を拾う書き方(EAFP)の 2 通りがあります。Python では後者が標準的とされています。

import os

# LBYL: 事前に確認する
if os.path.exists(path):
    with open(path) as f:      # 確認と実行の間に消される可能性がある
        data = f.read()

# EAFP: やってみて失敗を拾う(推奨)
try:
    with open(path) as f:
        data = f.read()
except FileNotFoundError:
    data = ""

# 辞書の場合はどちらでもよいが、get のほうが短い
value = d["key"] if "key" in d else 0
value = d.get("key", 0)

事前確認は確認した瞬間と実行する瞬間の間に状態が変わり得るという弱点があります。ファイルやネットワークのように、他者が同時に触るものは EAFP が安全です。

複数の例外をまとめて扱う

from contextlib import suppress

# 無視してよいと分かっている例外だけを明示的に握りつぶす
with suppress(FileNotFoundError):
    os.remove("temp.txt")           # 無ければ無視でよい

# 複数の処理をまとめて実行し、失敗を集めて報告する
errors = []
for path in paths:
    try:
        process(path)
    except Exception as e:
        errors.append((path, e))    # 1 件失敗しても続行する

if errors:
    for path, e in errors:
        print(f"{path}: {type(e).__name__}: {e}")
    raise RuntimeError(f"{len(errors)} 件の処理に失敗しました")

バッチ処理では「1 件失敗したら全部止める」か「失敗を記録して続行する」かを最初に決めます。suppress は「無視してよい」という意図をコードに残せる点が except: pass と違います。

リトライする

import time

def fetch_with_retry(url, attempts=3):
    last = None
    for i in range(1, attempts + 1):
        try:
            return fetch(url)
        except (TimeoutError, ConnectionError) as e:
            last = e
            if i < attempts:
                time.sleep(2 ** i)      # 1 回目 2 秒、2 回目 4 秒
    raise RuntimeError(f"{attempts} 回試して失敗しました") from last

再試行してよいのは一時的な失敗だけです。認証エラーや入力不正は何度やっても同じなので、対象の例外を絞ってください。

スタックトレースを文字列で受け取る

import traceback

try:
    1 / 0
except ZeroDivisionError as e:
    print(type(e).__name__)              # ZeroDivisionError
    print(str(e))                        # division by zero
    print(traceback.format_exc())        # 発生箇所まで含む全文
    print(e.__traceback__.tb_lineno)     # 行番号

例外を通知やレスポンスに載せるときは、利用者にはメッセージだけ、ログにはトレースバック全文と分けます。詳細は 例外を文字列で出力する方法 を参照してください。

主な組み込み例外

例外起きる場面
ValueError型は合っているが値がおかしい(int("abc")
TypeError型が違う("1" + 1
KeyError辞書に無いキー
IndexError範囲外の添字
AttributeError存在しない属性・メソッド(None に対する操作が多い)
FileNotFoundErrorファイルが無い(OSError の一種)
ZeroDivisionError0 で割った
ImportError / ModuleNotFoundErrorimport に失敗

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 標準ライブラリ
  5. 外部ライブラリ
  6. 制御構文
  7. リスト(配列)
  8. タプル
  9. セット
  10. 辞書(dict)
  11. クラスとメソッド
  12. 継承の概念と必要性
  13. 継承の構文
  14. コンストラクタ
  15. cookieの値の設定と取得
  16. 例外処理
  17. 例外を文字列で出力する方法
  18. httpリクエスト(curl)をする方法
  19. Responseオブジェクトの中身の確認
  20. 変数が空かどうか判定する方法
  21. タイムゾーンの設定と現在日時の取得と文字列化
  22. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ