概要
今回はコードレビュー中に指摘を頂いた2点数の内容について説明する。 Pythonをはじめとした一般的な言語には、組み込み例外が存在し、基本的にtry~exceptをを使用しない場合は、例外発生時ランタイムによってキャッチされる。 これをより良くするための一つの方法として、自作例外による例外のハンドリングがあるが、今回はその時に踏んだアンチパターンが2点あるので説明していく。
Goto文としての例外は使ってはいけない。
次の例を見てみよう。
>>> def div_ten(x): ... try: ... return 10//x ... except ZeroDivisionError: ... print("Could not division with 0") ... >>> div_ten(3) 3 >>> div_ten(0) Could not division with 0
この例ではx=0のとき組み込み例外ZeroDivisionError
が発生する。(正直名前でエラーがわかりやすいのであまり例外ハンドリングする意味がないが…)
例があまり良くないが、これは本来の例外処理としての意味を守った使い方であるので問題ない。
問題は次のような例である。(これも例は非常に悪い)
>>> div_ten(0) Could not division with 0 >>> def div_ten(x): ... try: ... if x==0: ... raise ValueError ... return 10//x ... except ValueError: ... # また全く別の計算
結局なにがいいたいのか?
例が最悪なので伝わりにくいと思うのだが、例外処理はexcept節内で処理を継続することが可能なので、意図的にif文のような使い方をすることができてしまう。
例外はその名の通り、意図しない値の伝搬時にアプリケーションがクラッシュしないように対処するための方法なので、それを超えた使い方をすると禁じてのGoto文と同じ用途となり、第三者が見たときにコードが非常に読みにくくなってしまう。
できるだけ自分がexcept節に記載している内容が、例外処理を超えた内容でないか意識するようにしたい。
raise文への誤解
上記の例でも使用したが、Pythonではraise文を使用することで、例外を発生させることが出来る。
>>> div_ten(0) Could not division with 0 >>> def div_ten(x): ... try: ... if x==0: ... raise ValueError ... return 10//x ... except ValueError: ... # また全く別の計算
ただし、ここで注意したいのはraiseのときに渡しているのは例外のインスタンスでないといけない、つまり
raise ValueError()
のようにインスタンス生成分として記載しなければクラスを渡すことになってしまうので誤りのように見えてしまう。
私はここらへんに意識が行っておらず、組み込み例外と同様に()
なしでraiseしていた。
結論、raiseに渡すのはクラスでもインスタンスでも良いということ
公式のドキュメントに以下のように説明がある。
Otherwise, raise evaluates the first expression as the exception object. It must be either a subclass or an instance of BaseException. If it is a class, the exception instance will be obtained when needed by instantiating the class with no arguments.
どうもPythonではraiseでクラスが渡されたときにインスタンスを自動生成される、つまり先程の例も正常となる。 実用上問題はないが、こういった曖昧な理解はバグの原因となるので、可能な限り厳密な理解ができるように努めたい。