# if 文と try 文、
どっちを使えばいいの?
Google の Python のコーディング規約を引用します。
例外は慎重に利用する。
Google Python スタイルガイド (opens new window)
try 文を使ってもいいの?
→ 頑張っても 例外が返される
可能性があるとき
あるいは
→ 可読性が良くなるとき
あるいは
→ 条件分岐が繰り返されて
実行速度が遅くなるとき
は try 文を使っても
いいかなと思います。
Python を習いたての頃は、例外は間違いを指摘してくれるものでした。 例えば SyntaxError は、嫌というほど見せられます。
def f(x)
return 2 * x
>>> def f(x)
File "<stdin>", line 1
def f(x)
^ セミコロンのつけ忘れ
SyntaxError: invalid syntax
>>>
ある程度、いろいろやっていくと例外を、 返り値の代わりに利用するようになったりします。
# リストの差分を取る。
def subtract_list(lst1, lst2):
lst = lst1.copy()
for element in lst2:
try:
lst.remove(element)
except ValueError:
pass
return lst
subtract_list([0, 1, 2], [0, 2])
# 1
ところで、この try 文で書かれた関数も if 文でも書くことができます。
# try 文から if 文に
def subtract_list(lst1, lst2):
lst = lst1.copy()
for element in lst2:
if element in lst:
lst.remove(element)
return lst
subtract_list([0, 1, 2], [0, 2])
# 1
果たして「どのような時に if 文ではなくtry 文を使うべき」なのでしょうか?
if 文を使い例外を発生しないようにして、 下記3つの場合 try 文で包んだ方が良いと考えていたりします。
- 頑張っても例外が投げ返される可能性がある場合
- 可読性が良くなる場合
- 速度が速くなる場合
# 1. なるべく if 文を使う
if 文を使い、例外が投げ返されるのを避けることができるのであれば、 try 文を使わないの方が望ましいのではないかと感じています。
例外については raise で投げる側と try で受け取る側があります。 ここでは try で受け取る側について考えていきます。 上記の言う「慎重」にとは、どう言う意味でしょうか? 「必要なとき以外は使わない」ということではないかなと感じます。
例外, try 文は読み辛いからです、これは個人の感想ですが。 その try 文が何を期待しているのか、またどこから例外が投げ込まれるのか、 一瞬考えてしまいます。
そのため try 文を記述するときに、 コメントで、なぜ、どうして try 文で包んでいるのか、 そして、どこから例外が発生するのかをコメントとして書くことが、多いような気がします。
例外は表現が少しフワフワしています。 そのコードの意図をつかむのに、少し考え込まないといけないのです。 反面 if 文で書く場合は、その意図が明白です。
例外の説明として次のようなものをよく見かけます。
try:
例外が発生する "かも" しれない処理
except:
例外が発生した時の処理
例外を説明する時に ZeroDivisionError
は最頻出です。
そして割り算は例外が発生する "かも" しれない処理です。
では割り算を書くときには、常に try 文を使わないといけないのでしょうか?
そうではないと考えます。
try:
d = a / b
except ZeroDivisionError:
...
try 文を使わなくて済むのであれば、
使わない方が望ましいと考えます。
if 文を使えば ZeroDivisionError
が発生しないようにすればいいのです。
実用的でもなく、わかりにくい例で、心苦しいのですが、 飴玉を人数分で割るコードについて考えて見たいと思います。 良い例が思いつき次第書き換え予定です。
...
---
飴玉a
整数を入力してください。
---
飴玉10
人数b
整数を入力してください。
---
飴玉10
人数0
人数に0は入力しないでください。
---
飴玉10
人数3
ひとり 3 個
あまり 1 個
>>>
'a'
, 'b'
などの文字列を打ち込むと
「整数を入力してください」と言う文字列を返してきます。
これを try 文を使って書くと以下のようになります。
#
# 対話モード >>> に
# コピペで動きます。
#
while True:
print('---')
# 1. 入力
try:
飴玉 = int(input('飴玉'))
人数 = int(input('人数'))
except ValueError:
print('整数を入力してください。')
continue
# 2. 計算
try:
ひとり, あまり = divmod(飴玉, 人数)
except ZeroDivisionError:
print('人数に0は入力しないでください。')
continue
# 3. 出力
print('ひとり', ひとり, '個')
print('あまり', あまり, '個')
break
このとき ValueError
は、どのタイミングで投げ返されるでしょうか?
int
に 'Hello, world!'
などの int
に変換できない
文字列が代入されたときに投げられます。
こういったコードを書いた筆者の気持ちを考えないといけないところが、
若干 try
文の苦しいところではないかなと感じています。
最初、本当に何意味をしてるのかわかりませんでした。
上記のコードは if 文で外すことができます。 なにが言いたいかというと try 文を使わなくても済むということです。
#
# 対話モード >>> に
# コピペで動きます。
#
while True:
print('---')
# 1. 入力
飴玉 = input('飴玉')
if not 飴玉.isdigit():
print('整数を入力してください。')
continue
人数 = input('人数')
if not 人数.isdigit():
print('整数を入力してください。')
continue
if not int(人数):
print('人数に0は入力しないでください。')
continue
飴玉, 人数 = map(int, (飴玉, 人数))
# 2. 計算
ひとり, あまり = divmod(飴玉, 人数)
# 3. 出力
print('ひとり', ひとり, '個')
print('あまり', あまり, '個')
break
上記の例もイマイチで、一概にこっちがいいとは言い切れないのですが、 try 文を使うと読みづらくなる気がします。
# 2. 頑張っても例外を投げ返される可能性がある場合
# 2.1. 組み込み関数 open
例えば、ファイル操作は、失敗する "かも" しれない操作です。 組み込み関数 open (opens new window) はその代表です。 そして open は if 文で事前に例外の発生するかしないかを確認することができません。
以下の記事は Python 2 に関するものですが、とても参考になります。 Python の昔の公式ドキュメントの見解です。
以下は非常にありがちな悪い見本です:
def get_status(file): if not os.path.exists(file): print "file not found" sys.exit(1) return open(file).readline()
ここで、 os.path.exists() を呼んでから open() を 呼ぶまでの間にファイルが消された場合を考えてください。 そうなれば最後の行は IOError を投げるでしょう。 同じことは、 file は存在しているけれど読み出し権限がなかった、 という場合にも起こります。
とにかくやってみないとわからないんだから、if 文で事前には確認できないやろという話をしています。
# 2.2. 標準ライブラリ shutil
標準ライブラリ shutil (opens new window) にファイルをコピーする関数 copyfile (opens new window) があります。
from shutil import copyfile
copyfile('src.txt', 'dst.txt')
shutil.copyfile(src, dst, *, follow_symlinks=True) (opens new window)
src という名前のファイルの内容 (メタデータを含まない) を dst という名前のファイルにコピーし、dst を返します。
上でご紹介させていただいた 関数のソースコードでも if 文で事前確認するのではなく、 失敗するかもしれないけとりあえず実行するような方針でコードが書かれています。
上記リンク先のコードでは with
文で書かれていますが、
以下では、いったん try
文で書いたものをご紹介したのちに、
with
文で書かれたコードを見ていきます。
# 2.2.1. try 文
汚くなってしまうのですが意図を把握するために try
文で書き直してみました。
copyfile
関数と同じものを try
文で書くと、
だいたい以下のようになります。
# かなり削りました。
def copyfile(src, dst):
fsrc = open(src, 'rb') # <--- ここで失敗しても
# open できていないので
# try 文で包む必要がありません。
try:
fdst = open(dst, 'wb')
try:
copyfileobj(fsrc, fdst)
finally:
fdst.close() # <--- ここで失敗するかもしれない
finally:
fsrc.close() # <--- ここでも失敗するかもしれない
# これは shutil.py からコピペしてます。
def copyfileobj(fsrc, fdst, length=16*1024):
"""copy data from file-like object fsrc to file-like object fdst"""
while 1:
buf = fsrc.read(length)
if not buf:
break
fdst.write(buf)
copyfile('src.txt', 'dst.txt')
ここでの考え方は「開いたファイルは閉じて欲しい」ということです。
そしてファイルを閉じるという作業は try
文が成功しようが失敗しようが、
関係なく実行して欲しい作業です。
そのような場合には finally
節を使います。
finally
節には例外の発生の有無に関わらず必ず処理を実行する処理を記述します。
try:
例外が発生する可能性のある処理
finally:
例外が発生するかしないかに関わらず実行する処理
open
関数と fdst.write
メソッドは、失敗する可能性があります。
ファイルが開けないことがあるからです。
たとえば他のアプリケーションが、
そのファイルを削除してしまっていた場合、書くことができません。
上記のコードでは except
節がなく、例外が投げられっぱなしになります。
そのため失敗しても処理を継続したい場合は copyfile
関数を呼び出した側で、
try 文で包む必要があります。
ファイルはこっちで閉じるけど、
例外の扱いは copyfile
関数を呼び出したそっち側にお任せしますよ、という設計になっています。
try:
copyfile('src.txt', 'dst.txt')
except OSError as error:
...
ただし finally
節の中の close
ファイルを閉じるという処理も
失敗する可能性があるので留意が必要です。
この辺りの例外への対応は正直自分もまだあまりわかっていません。
頑張って閉じては見るけど、
できなかったらごめんなさいと言う設計と言うことでしょうか。
finally:
fdst.close() # <--- ここで失敗するかもしれない
finally:
fsrc.close() # <--- ここでも失敗するかもしれない
# 2.2.2. with 文
with 文の方が綺麗ですね。 ちょっと話を with 文に脱線させます。 copyfile 関数は with 文が使われています。 with 文を使ってファイルを open しているとこにだけご注目ください。 実際のコードは shutil.py (opens new window) をご参照ください。
# かなり削りました。
def copyfile(src, dst):
with open(src, 'rb') as fsrc: # <--- with 文でファイルを
with open(dst, 'wb') as fdst: # open している。
copyfileobj(fsrc, fdst)
# これは shutil.py からコピペしてます。
def copyfileobj(fsrc, fdst, length=16*1024):
"""copy data from file-like object fsrc to file-like object fdst"""
while 1:
buf = fsrc.read(length)
if not buf:
break
fdst.write(buf)
copyfile('src.txt', 'dst.txt')
with 文の動作は以下のようなものです。
__enter__
メソッドで開始時の動作を規定し、
__exit__
メソッドで終了時の動作を規定します。
__exit__
メソッドは finally 節と同じように、
例外が起こった時にも必ず処理が実行されます。
# 対話モードにコピペで動きます。
class Context:
def __enter__(self):
print('enter')
def __exit__(self, exc_type, exc_value, traceback):
print('exit')
with Context():
pass
# enter
# exit
with Context():
raise Exception
# enter
# exit <--- __exit__ メソッドが実行されてから例外が投げられる。
# Traceback (most recent call last):
# File "<stdin>", line 2, in <module>
# Exception
# 2.2.3. OSError
OSError
は IO に関連した例外クラスの親クラスになっているので、
OSError
を捕まえておけばいいかなと...
とりあえず、ごくざっくりの枠で捕まえて詳細は、捕まえてから中をみる感じです。
exception OSError([arg]) (opens new window)
この例外はシステム関数がシステム関連のエラーを返した場合に送出されます。 例えば "file not found" や "disk full" のような I/O の失敗が発生したときです (引数の型が不正な場合や、他の偶発的なエラーは除きます)。
# 例外のクラス階層
# https://docs.python.org/ja/3/library/exceptions.html#exception-hierarchy
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
>>> # 対話モードからだと
>>> # 全ての組み込み型の継承関係を確認できます。
>>> help(__builtins__)
# 3. try 文を使った方が可読性が良くなる場合
結局 Python において可読性は、正義です。
可読性は重要である。
Readability counts.
PEP 20 - The Zen of Python (opens new window)
# 3.1. C++ の例
Python のコードで具体例を示せなくて大変申し訳ないのですが、 以下は C++ のコードになってしまうのですが、とてもわかりやすいです。
これは大量の if 文をたった1つの try 文で括ってしまっています。 すると例外が、なぜ、どこから、発生するのかをコメントに書く価値が生じます。 この辺りは、バランス感覚があるのかなという気がします。
ちなみに Google の C++ のコーディング規約では、以下のように書かれています。
C++ の例外は使いません。
例外 - Google C++ スタイルガイド 日本語全訳 (opens new window)
上記のリンク先の文章では、 例外を使うことの危険性や可読性を悪くしてしまうことが指摘されています。 try 文に対する温度感は Python のコーディング規約と近いものがあります。
しかし、なぜ「例外は、使いません」という、より激しい表現なのでしょうか? Python では「例外は、慎重に使う」でした。 これは例外が重いからだと思われます。 C++ で書かれる製品は処理速度が優先される気配を感じます。例外は重たいのです。
# 3.2. try の範囲はなるべく狭く
上記のサンプルコードでは広い範囲が try 文で囲われていました。 一般に try 文で囲む範囲は狭い方が良いとされています。
まず第一に、範囲が広いと try 文がなにを意図しているのか掴むことが困難になります。 これはコメントを書く必要が生じます。
また第二に、想定していない例外を掴んでしまう可能性があります。 これは Google の Python のコーディング規約にも書かれています。
Minimize the amount of code in a try/except block. The larger the body of the try, the more likely that an exception will be raised by a line of code that you didn't expect to raise an exception. In those cases, the try/except block hides a real error.
2.4 Exceptions - Google Python Style Guide (opens new window)
# 4. 繰り返し条件分岐を行う場合
問題: 以下のように事前に例外が発生するかどうかを
if
文で事前に確認してから処理を実行する書き方と
for _ in range(1000):
if 例外が発生するか?:
...
else:
...
try
文で処理を実行させて例外が発生した場合だけ
捕まえる書き方、どちらが速いでしょうか?
for _ in range(1000):
try:
...
except:
...
答え: try
文が速いことが多い気がします。
もう少し詳しくいうと、
例外が発生する回数が多い場合は if
文が、少ない場合は try
文が速いです。
なぜでしょうか?それは実は内部的には例外が発生するかどうかを Python は条件分岐しているからです。 例えば CPython の実装を見ると C 言語で書かれた大量の条件分岐のコードを見ます。
if
文で判定することは CPython でも判定して、
そして Python でも判定するという2度手間を踏んでいるのです。
Python では例外は発生してしまうと重たいのですが、
発生しなければ重たくないのです。
多い少ないを定量的に示すことは難しいのですが、 以下2例をご紹介いたします。
# 4.1. イテレータ - StopIteration
StopIteration
は Python の for 文の中で意図的に使われている例外です。
Python は for 文が呼ばれると StopIteration
が呼び出されるまで for 文を回し続けます。
#
# 対話モード >>> に
# コピペで実行できます。
#
class Iterator:
def __init__(self, sequence):
self._sequence = sequence
self._index = 0
def __iter__(self):
return self
def __next__(self):
self._index += 1
if self._index < len(self._sequence):
return self._sequence[self._index]
else:
raise StopIteration
#
# while 文
#
iterator = Iterator([3, 4, 1, 2, 0])
while True:
try:
e = next(iterator)
except StopIteration:
break
else:
print(2 * e)
#
# for 文
# 上記の while 文は
# 下記の for 文と等価です。
#
iterator = Iterator([3, 4, 1, 2, 0])
for e in iterator:
print(2 * e)
...
8
2
4
0
>>>
イテレータにつきまして、
なぜ終了の判定を例外 StopIteration
で行なっているかも含めて
以下のページでご紹介させていただきました。
# 4.2. リストの差分 - ValuError
try 文を使いリストの差分を求める関数を作ることができます。
def subtract_list(lst1, lst2):
lst = lst1.copy()
for e2 in lst2:
try:
lst.remove(e2)
except ValueError:
continue
return lst
subtract_list([0, 1, 2], [0, 1, 2])
subtract_list([0, 1, 2], [0])
subtract_list([0, 1, 2], [0, 1])
subtract_list([0], [0, 1, 2])
subtract_list([0, 0, 1, 1, 2, 2], [0, 1, 2])
>>> subtract_list([0, 1, 2], [0, 1, 2])
[]
>>> subtract_list([0, 1, 2], [0])
[1, 2]
>>> subtract_list([0, 1, 2], [0, 1])
[2]
>>> subtract_list([0], [0, 1, 2])
[]
>>> subtract_list([0, 0, 1, 1, 2, 2], [0, 1, 2])
[0, 1, 2]
>>>
なぜ上記のようなコードの書き方をしているかについての詳細は 以下の記事で書かせていただきました。
# 4.3. まとめ
イテレータの例から速く処理が完了する場合は、try
文を使ってもいいかなと感じます。
ただ Python は C++ とは異なりもともとかなり遅い言語です。
可読性を犠牲にして処理を速くしても、正直得られる恩恵はそこまで大きくはないかなと感じています。 このときは try 文でこのときは if 文でという判断基準は示すことはできませんが、 気楽に if 文を使ってしまえばいいかなと思います。
# 5. LBYL と EAFP
try 文を使うか使わないかという話は、try 文を使うか、それとも if 文を使うかに言い換えることができます。 ここで EAFP と LBYL という言葉をご紹介いたします。
例外処理や Validation には LBYL(Look Before You Leap) と EAFP(Easier to Ask for Forgiveness than Permission) という概念があり、 要は前者は「石橋を叩いて渡る」方式で、後者は「当たって砕けろ」方式です。
Pythonで例外を投げるときのベストプラクティス - Qiita (opens new window)
EAFP - 用語集 (opens new window)
「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission、マーフィーの法則)」の略です。 この Python で広く使われているコーディングスタイルでは、 通常は有効なキーや属性が存在するものと仮定し、その仮定が誤っていた場合に例外を捕捉します。 この簡潔で手早く書けるコーディングスタイルには、 try 文および except 文がたくさんあるのが特徴です。 このテクニックは、C のような言語でよく使われている LBYL スタイルと対照的なものです。
LBYL - 用語集 (opens new window)
「ころばぬ先の杖 (look before you leap)」 の略です。 このコーディングスタイルでは、呼び出しや検索を行う前に、 明示的に前提条件 (pre-condition) 判定を行います。 EAFP アプローチと対照的で、 if 文がたくさん使われるのが特徴的です。マルチスレッド化された環境では、 LBYL アプローチは "見る" 過程と "飛ぶ" 過程の競合状態を引き起こすリスクがあります。 例えば、if key in mapping: return mapping[key] というコードは、 判定の後、別のスレッドが探索の前に mapping から key を取り除くと失敗します。 この問題は、ロックするか EAFP アプローチを使うことで解決できます。
公式の用語集に説明があるのは、この記事から知ることができました。 ありがとうございます。
# 6. 検査例外
僕は初めて例外という機能を見たとき、 もしかしてありとあらゆる処理は例外が発生しうるのではないのか? という思いに狩られました。 もはやキチガイです。当時は真剣でした。
頑張っても例外が発生してしまうものと、
頑張れば例外が発生しないものの2種類が存在します。
例えば割り算をするときの ZeroDivisionError
は if 文を使えば例外を避けることができますが、
ファイルを開くときの OSError
は if 文を使っても例外が発生することを避けることはできません。
全ての処理は例外が発生しうるのではないか?という疑問に対する答えは Java や Swift の検査例外が教えてくれます。
# 6.1. Java の検査例外
Java の例外は体系的にまとまっています。 以下のリンク先は理解はできていないのですが、全体像の雰囲気を把握するにはちょうど良いかなと思います。
Python の組み込み例外では、例外 Exception
とエラー Error
の表現が混在しています。
Java はこの2つを明確に分けています。
Java では Exception
は try 文で捕まえられますが、Error
は捕まえることができません。
Python で try で捕まえられない例外ってあるのかなと、ちょっと疑問に思ったのですが、 まだ探せていません。
VirtualMachineError
が該当するそうです。日本語訳すると「仮想マシンエラー」でしょうか。
仮想マシンの胴体の方がやられたら、顔の交換とかでは復活できなさそうな気はします..
Java 仮想マシンが壊れているか、または動作を継続するのに必要なリソースが足りなくなったことを示すためにスローされます。
クラス VirtualMachineError - Oracle Docs (opens new window)
Java では Exception
は2種類に分けられます。
上記 Java の例で言えば Exception
は頑張っても発生してしまう例外で、
RuntimeException
は頑張れば避けることのできる例外です。
で、頑張っても避けることができない Exception
は、
検査例外checked exception とも呼ばれます。
確かに Exception
だと Runtime Exception
なのか
checked exception なのか判別できないですからね。
こいつを投げてくる関数を書くときは必ず try 文で包まないと、 コンパイラにエラーで弾かれます。
# 6.2. 検査例外は嫌われ者
とっても安全そうな検査例外ですが、Kotlin では外されました。 Effective Java とか Qiita の記事を読んだのですが、 検査例外のために try を書くのは、とっても面倒らしいです。
確かに個人使用の簡単なツールを書く時には try 文は、ならいらないと思います。 例外が投げ返されて処理が止まっても、再起動すればいいだけだからです。
# 6.3. Swift の検査例外
Swift では検査例外の機能が、少し削られています。 まだ読めていませんが、 どういう削り方をしたのか、Swift の記事を引用していきたいと思います。 この辺りのさじ加減というのは、もはや芸術の領域だと思います。
- Swiftのエラー4分類が素晴らしすぎるので... - Qiita (opens new window)
- SwiftはどのようにJavaの検査例外を改善したか - Qiita (opens new window)
Java は Python で言う所の関数が書けません。 どういうことかというと、必ずクラスに所属したメソッドでなければいけないのです。
Java の後続の Kotlin や Swift では関数で定義できます。 Java が理論的に純粋なものを組み上げてくれたものを、 緩くできるところは緩くするというアプローチで改善された感を感じます。
# 7. おわりに
ここまで以下のような流れで見てきました。
try 文は可読性を下げてしまうので使うときは慎重に、 そして必要なとき以外は if 文を使うのが望ましいかなと思います。 必要な時というのは以下の3つの場合です。
- 頑張っても例外が投げられてしまう時
- 可読性が上がる時
- 処理が速くなる時
1 の頑張っても例外が投げ返されてしまうケースは try 文で包んだ方が良さそうです。 ただし、個人で使うツールはいらないかなと思います。 なぜならスクリプトを再起動すればいいだけですし、try 文を書くのも読むのも面倒だからです。
2 の可読性が良くなる場合は、人によりけりなので判断が凄く難しいなと思います。 それでもスッキリ書けて合意が取れるのであれば、許されるのかなと思ったりもします。
3 の処理速度は、そうは滅多にないんじゃないかなと思ったりします。 その箇所だけなら何倍か速くできるかもしれませんが、 全体から見ればあまり改善されないような気がするからです。
以上になります。ありがとうございました。
例外は一定の条件でのみ利用する。
例外 - Google Python スタイルガイド (opens new window)