nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonで例外を捕捉するときに、except:やexcept BaseException:と書いてはいけません。except Exception:またはもっと具体な例外クラスを指定しましょう

はじめに

RAISE THE DREAM!!! nikkieです。

ミリアニ12話を思い出すだけで泣ける身体にされてしまった1のですが、今回は技術ネタ。
Pythonraiseするものといったら、例外!2
Pythonの例外の扱い、具体的に言うとtry ... exceptのexcept節について取り上げます。

目次

きっかけ:『Python実践レシピ』を読んでいて

3章が言語仕様を扱う章で、3.1が例外処理です。
「3.1.6 例外処理:よくあるエラーと対処法」の中に

また、except節で例外の種類を指定しなかったり、except Exception:のように基底クラスを指定してしまうのも推奨されません。(Kindle版 p.128)

とあります。
私は「あれ、そうだったっけ?」と思ったのです

結論:except節の書き方

  • オススメしない書き方(ぶっぶーですわ🙅‍♂️)
    • except:と例外の種類を指定しない
    • except BaseException:(すべての例外の基底クラスを指定)
      • この2つが同じことはPEP 8にあります
  • オススメする書き方
    • except Exception:(ユーザ定義例外の基底クラスを指定)
      • この点、『Python実践レシピ』にはお問い合わせしてきます
    • 例外が既知であればExceptionよりもより具体な例外のクラスを指定したい(『Python Distilled』参照)

BaseExceptionとExceptionは何が違うの?

まずBaseExceptionとExceptionの区別です。

「組み込み例外」のドキュメントより

「組み込み例外」のドキュメントの「例外のクラス階層」より
https://docs.python.org/ja/3/library/exceptions.html#exception-hierarchy

  • BaseExceptionがすべての例外の基底クラス
  • ExceptionはBaseExceptionを継承している
  • BaseExceptionを継承しているのはExceptionだけではない

同じドキュメントの「基底クラス」も見ましょう。
https://docs.python.org/ja/3/library/exceptions.html#base-classes

  • BaseException
    • 全ての組み込み例外の基底クラスです。

    • ユーザ定義の例外に直接継承されることは意図されていません (継承には Exception を使ってください)。

  • Exception
    • システム終了以外の全ての組み込み例外はこのクラスから派生しています。

    • 全てのユーザ定義例外もこのクラスから派生させるべきです。

BaseExceptionとExceptionの違い

  • BaseExceptionは、全ての組み込み例外の基底クラス
    • 子クラスとして、Exception、KeyboardInterrupt、SystemExit(など)
  • Exceptionはユーザ定義例外の基底クラス
    • Exceptionを継承した組み込み例外もある

except節と例外の継承関係

Pythonチュートリアルの「8.3. 例外を処理する」を参照します。
https://docs.python.org/ja/3/tutorial/errors.html#handling-exceptions

except 節のクラスは、例外と同じクラスか基底クラスのときに互換 (compatible)となります。

Pythonチュートリアルのサンプルコード

  • BはExceptionを継承したクラス
  • CはBを継承(もちろんExceptionも継承している)
for cls in [B, C]:
    try:
        raise cls()
    except C:
        print("C")
    except B:
        print("B")
B
C
>>> isinstance(B(), C)
False
  • Cは1つ目のexcept節で処理

except節を入れ替えると

for cls in [B, C]:
    try:
        raise cls()
    except B:
        print("B")
    except C:
        print("C")
B
B
  • Bは1つ目のexcept節で処理
  • CのインスタンスはBなので、Cも1つ目のexcept節で処理
    • Bが基底クラスという継承関係!
>>> isinstance(C(), B)
True

BaseExceptionとExceptionに読み替えると

  • except Exception:はBaseExceptionを捕捉しない
    • BaseExceptionを継承しているが、Exceptionを継承していないKeyboardInterruptやSystemExitを捕捉しない
  • except BaseException:
    • BaseExceptionを継承しているExceptionを捕捉する
    • BaseExceptionを継承しているKeyboardInterruptやSystemExitも捕捉する

except BaseException:と書くと、KeyboardInterruptやSystemExitも捕捉してしまうのが望ましくない挙動と考えます。
なぜならこれらのドキュメントには

Exception をキャッチするコードに誤ってキャッチされないように、Exception ではなく BaseException を継承しています。(SystemExit

のように書いてあるからです。

PEP 8の「Programming Recommendations」より

https://peps.python.org/pep-0008/#programming-recommendations

例外に関してもいくつか記載されています

Derive exceptions from Exception rather than BaseException.

ユーザ定義例外は、BaseExceptionではなくExceptionを継承しようという話ですね。

A bare except: clause will catch SystemExit and KeyboardInterrupt exceptions, (略)

例外のクラスを指定しないexcept:をbare(むき出しの)と言っています。

bare except is equivalent to except BaseException:

bare exceptはexcept BaseException:と同じなので、SystemExitやKeyboardInterruptを捕捉する

If you want to catch all exceptions that signal program errors, use except Exception:

プログラムのエラーを合図するすべての例外を捕捉したいときはexcept Exception:を使おう、とあります。

PEP 8にあるので、flake8も検出してくれるみたいですね

Python Distilled』より

3.4で例外を扱っています。

分かりやすいなと思ったのは、以下の説明

  • プログラムに関連するエラーはExceptionを継承
    • (感想)ValueError(など)や、ユーザ定義例外のことを「プログラムに関連するエラー」と呼ぶのうまい
  • 制御フローの変更に使われる例外
    • SystemExitやKeyboardInterruptなど(3.4.2)

3.4.6にアドバイスがいくつかあります

エラーを捕捉する際は、except節で捕捉する例外の範囲を限定しましょう。

except Exceptionと書いてしまうと、無視できない正当なプログラミングエラーも捕捉することになります。デバッグが難しくなるので、これはやめておきましょう。

これを受けての私の意見ですが、

  • Python Distilled』の主張には賛成。例外の範囲を限定したい
  • 現実的には、どんな例外が送出されるか分かりきっておらず、範囲を限定できないケースもあると思う。そんなときに限ってはexcept Exception:と書くのもOKとしたい
    • 限定できたら書き直そう
    • 似ているからと言って、except BaseException:と書いてはいけません(ここまで見てきたとおりです)

終わりに

Pythonのexceptの書き方を見てきました。
except節に書くクラスは理解がふんわりしていたのですが、ドキュメントや『Python Distilled』にあたって、BaseExceptionはよくなくてExceptionはよいということがなぜかまで含めて理解できました🙌


  1. ありがとう😭