Last Updated: 2/6/2024, 5:44:57 AM

# if 文と try 文、
どっちを使えばいいの?

なるべく if 文を使います。

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. 頑張っても例外が投げ返される可能性がある場合
  2. 可読性が良くなる場合
  3. 速度が速くなる場合

# 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 文を使うと読みづらくなる気がします。

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 は存在しているけれど読み出し権限がなかった、 という場合にも起こります。

例外 - Python 良い慣用句、悪い慣用句 (opens new window)

とにかくやってみないとわからないんだから、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 の記事を引用していきたいと思います。 この辺りのさじ加減というのは、もはや芸術の領域だと思います。

Java は Python で言う所の関数が書けません。 どういうことかというと、必ずクラスに所属したメソッドでなければいけないのです。

Java の後続の Kotlin や Swift では関数で定義できます。 Java が理論的に純粋なものを組み上げてくれたものを、 緩くできるところは緩くするというアプローチで改善された感を感じます。

# 7. おわりに

ここまで以下のような流れで見てきました。

try 文は可読性を下げてしまうので使うときは慎重に、 そして必要なとき以外は if 文を使うのが望ましいかなと思います。 必要な時というのは以下の3つの場合です。

  1. 頑張っても例外が投げられてしまう時
  2. 可読性が上がる時
  3. 処理が速くなる時

1 の頑張っても例外が投げ返されてしまうケースは try 文で包んだ方が良さそうです。 ただし、個人で使うツールはいらないかなと思います。 なぜならスクリプトを再起動すればいいだけですし、try 文を書くのも読むのも面倒だからです。

2 の可読性が良くなる場合は、人によりけりなので判断が凄く難しいなと思います。 それでもスッキリ書けて合意が取れるのであれば、許されるのかなと思ったりもします。

3 の処理速度は、そうは滅多にないんじゃないかなと思ったりします。 その箇所だけなら何倍か速くできるかもしれませんが、 全体から見ればあまり改善されないような気がするからです。

以上になります。ありがとうございました。

例外は一定の条件でのみ利用する。
例外 - Google Python スタイルガイド (opens new window)