8. エラーと例外

これまでエラーメッセージについては簡単に触れるだけでしたが、チュートリアル中の例を自分で試していたら、実際にいくつかのエラーメッセージを見ていることでしょう。エラーには (少なくとも) 二つのはっきり異なる種類があります。それは 構文エラー (syntax error)例外 (exception) です。

8.1. 構文エラー

構文エラーは構文解析エラー (parsing error) としても知られており、Python を勉強している間に最もよく遭遇する問題の一つでしょう:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

The parser repeats the offending line and displays little arrows pointing at the token in the line where the error was detected. The error may be caused by the absence of a token before the indicated token. In the example, the error is detected at the function print(), since a colon (':') is missing before it. File name and line number are printed so you know where to look in case the input came from a script.

8.2. 例外

たとえ文や式が構文的に正しくても、実行しようとしたときにエラーが発生するかもしれません。実行中に検出されたエラーは 例外 (exception) と呼ばれ、常に致命的とは限りません。これから、Python プログラムで例外をどのように扱うかを学んでいきます。ほとんどの例外はプログラムで処理されず、以下に示されるようなメッセージになります:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    10 * (1/0)
          ~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    4 + spam*3
        ^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    '2' + 2
    ~~~~^~~
TypeError: can only concatenate str (not "int") to str

エラーメッセージの最終行は何が起こったかを示しています。例外は様々な型 (type) で起こり、その型がエラーメッセージの一部として出力されます。上の例での型は ZeroDivisionError, NameError, TypeError です。例外型として出力される文字列は、発生した例外の組み込み名です。これは全ての組み込み例外について成り立ちますが、ユーザ定義の例外では (成り立つようにするのは有意義な慣習ですが) 必ずしも成り立ちません。標準例外の名前は組み込みの識別子です (予約語ではありません)。

残りの行は例外の詳細で、その例外の型と何が起きたかに依存します。

エラーメッセージの先頭部分では、例外が発生した実行コンテキスト (context) を、スタックのトレースバック (stack traceback) の形式で示しています。一般には、この部分にはソースコード行をリストしたトレースバックが表示されます。しかし、標準入力から読み取られたコードは表示されません。

組み込み例外 には、組み込み例外とその意味がリストされています。

8.3. 例外を処理する

例外を選別して処理するようなプログラムを書くことができます。以下の例を見てください。この例では、有効な文字列が入力されるまでユーザに入力を促しますが、ユーザがプログラムに (Control-C か、またはオペレーティングシステムがサポートしている何らかのキーを使って) 割り込みをかけてプログラムを中断させることができるようにしています。ユーザが生成した割り込みは、 KeyboardInterrupt 例外が送出されることで通知されるということに注意してください。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

try 文は下記のように動作します。

  • まず、 try 節 (try clause) (キーワード tryexcept の間の文) が実行されます。

  • 何も例外が発生しなければ、 except 節 をスキップして try 文の実行を終えます。

  • try 節内の実行中に例外が発生すると、その節の残りは飛ばされます。次に、例外型が except キーワードの後に指定されている例外に一致する場合、except 節が実行された後、 try/except ブロックの後ろへ実行が継続されます。

  • もしも except 節 で指定された例外と一致しない例外が発生すると、その例外は try 文の外側に渡されます。例外に対するハンドラ (handler、処理部) がどこにもなければ、 処理されない例外 (unhandled exception) となり、エラーメッセージを出して実行を停止します。

一つの try 文には複数の except 節 が付けられ、別々の例外に対するハンドラを指定できます。 多くとも一つのハンドラしか実行されません。 ハンドラは対応する try 節 内で発生した例外だけを処理し、同じ try 節内の別の例外ハンドラで起きた例外は処理しません。 except 節 では丸括弧で囲ったタプルという形で複数の例外を指定できます。例えば次のようにします:

... except (RuntimeError, TypeError, NameError):
...     pass

except 節 内のクラスは、そのクラスや派生クラスのインスタンスである例外とマッチします(その逆はありません --- 派生クラスが書かれた except節 は、その基底クラスのインスタンスにはマッチしません)。例えば、次のコードは B, C, D の順に出力されます:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

except 節 が逆に並んでいた場合 (except B が最初にくる場合)、 B, B, B と出力されるはずだったことに注意してください --- 最初に一致した except 節 が駆動されるのです。

例外が発生するとき、例外は関連付けられた値を持つことができます。この値は例外の 引数 (arguments) とも呼ばれます。引数の有無および引数の型は、例外の型に依存します。

except節は例外名の後に変数を指定できます。その変数は例外インスタンスに紐付けられ、一般的には引数を保持する args 属性を持ちます。利便性のため、組み込み例外型には __str__() が定義されており、明示的に .args を参照せずとも すべての引数を表示できます。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

例外の __str__() 出力は、処理されない例外のメッセージ末尾 ('詳細') として表示されます。

BaseException はすべての例外に共通する基底クラスです。そのサブクラスの一つである Exception は、致命的でない例外すべての基底クラスです。 Exception のサブクラスではない例外は、一般的にプログラムが終了することを示すために使われているため処理されません。例えば、sys.exit() によって送出される SystemExit や、ユーザーがプログラムを中断させたいときに送出される KeyboardInterrupt があります。

Exception はほぼすべての例外を捕捉するワイルドカードとして使えます。しかし良い例外処理の手法とは、処理対象の例外の型をできる限り詳細に書き、予期しない例外はそのまま伝わるようにすることです。

Exception に対する最も一般的な例外処理のパターンでは、例外を表示あるいはログ出力してから再度送出します(呼び出し側でも例外を処理できるようにします):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

try ... except 文には、オプションで else 節 (else clause) を設けることができます。 else 節を設ける場合、全ての except 節よりも後ろに置かなければなりません。 else 節は try 節 で全く例外が送出されなかったときに実行されるコードを書くのに役立ちます。例えば次のようにします:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

追加のコードを付け加えるのは try 節よりも else 節の方がよいでしょう。 なぜなら、そうすることで try ... except 文で保護されたコードから送出されたもの以外の例外を過って捕捉してしまうという事態を避けられるからです。

例外ハンドラは、try 節 の直接内側で発生した例外を処理するだけではなくその try 節 から (たとえ間接的にでも) 呼び出された関数の内部で発生した例外も処理します。例えば:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

8.4. 例外を送出する

raise 文を使って、特定の例外を発生させることができます。例えば:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

raise の唯一の引数は送出される例外を指し示します。 これは例外インスタンスか例外クラス(BaseException を継承したクラス、たとえば Exception やそのサブクラス)でなければなりません。 例外クラスが渡された場合は、引数無しのコンストラクタが呼び出され、暗黙的にインスタンス化されます:

raise ValueError  # shorthand for 'raise ValueError()'

例外が発生したかどうかを判定したいだけで、その例外を処理するつもりがなければ、単純な形式の raise 文を使って例外を再送出させることができます:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise NameError('HiThere')
NameError: HiThere

8.5. 例外の連鎖

except 節の中で未処理の例外が発生した場合、その未処理の例外は処理された例外のエラーメッセージに含まれます:

>>> try:
...     open("database.sqlite")
... except OSError:
...     raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    open("database.sqlite")
    ~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error

ある例外が他の例外から直接影響されていることを示すために、raise 文にオプションの from 句を指定します:

# exc must be exception instance or None.
raise RuntimeError from exc

これは例外を変換するときに便利です。例えば:

>>> def func():
...     raise ConnectionError
...
>>> try:
...     func()
... except ConnectionError as exc:
...     raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    func()
    ~~~~^^
  File "<stdin>", line 2, in func
ConnectionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database

また、自動的な例外の連鎖を無効にするには from None を指定します。

>>> try:
...     open('database.sqlite')
... except OSError:
...     raise RuntimeError from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError from None
RuntimeError

例外の連鎖の仕組みに関して、詳しくは 組み込み例外 を参照してください。

8.6. ユーザー定義例外

プログラム上で新しい例外クラスを作成することで、独自の例外を指定することができます (Python のクラスについては クラス 参照)。例外は、典型的に Exception クラスから、直接または間接的に派生したものです。

例外クラスでは、普通のクラスができることなら何でも定義することができますが、通常は単純なものにしておきます。大抵は、いくつかの属性だけを提供し、例外が発生したときにハンドラがエラーに関する情報を取り出せるようにする程度にとどめます。

ほとんどの例外は、標準の例外の名前付けと同様に、"Error" で終わる名前で定義されています。

多くの標準モジュールでは、モジュールで定義されている関数内で発生する可能性のあるエラーを報告させるために、独自の例外を定義しています。

8.7. クリーンアップ動作を定義する

try 文にはもう一つオプションの節があります。この節はクリーンアップ動作を定義するためのもので、どんな状況でも必ず実行されます。例を示します:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

もし finally 節がある場合、 try 文が終わる前の最後の処理を、 finally 節が実行します。 try 文が例外を発生させるか否かに関わらず、 finally 節は実行されます。以下では、例外が発生するという更に複雑なケースを議論します:

  • もし try 文の実行中に例外が発生したら、その例外は except 節によって処理されるでしょう。もしその例外が except 節によって処理されなければ、 finally 節が実行された後に、その例外が再送出されます。

  • except 節または else 節の実行中に例外が発生することがあり得ます。その場合も、 finally 節が実行された後に例外が再送出されます。

  • finally 節で breakcontinue または return 文が実行された場合、例外は再送出されません。

  • もし try 文が break 文、 continue 文または return 文のいずれかに達すると、その break 文、 continue 文または return 文の実行の直前に finally 節が実行されます。

  • もし finally 節が return 文を含む場合、返される値は try 節の return 文ではなく、finally 節の return 文によるものになります。

例えば:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

より複雑な例:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

見てわかるとおり、 finally 節はどの場合にも実行されています。 文字列で割り算をすることで発生した TypeErrorexcept 節で処理されていないので、 finally 節実行後に再度送出されています。

実世界のアプリケーションでは、 finally 節は(ファイルやネットワーク接続などの)外部リソースを、利用が成功したかどうかにかかわらず解放するために便利です。

8.8. 定義済みクリーンアップ処理

オブジェクトのなかには、その利用の成否にかかわらず、不要になった際に実行される標準的なクリーンアップ処理が定義されているものがあります。以下の、ファイルをオープンして内容を画面に表示する例をみてください。

for line in open("myfile.txt"):
    print(line, end="")

このコードの問題点は、コードの実行が終わった後に不定の時間ファイルを開いたままでいることです。これは単純なスクリプトでは問題になりませんが、大きなアプリケーションでは問題になりえます。 with 文はファイルのようなオブジェクトが常に、即座に正しくクリーンアップされることを保証します。

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

この文が実行されたあとで、たとえ行の処理中に問題があったとしても、ファイル f は常に close されます。ファイルなどの、定義済みクリーンアップ処理を持つオブジェクトについては、それぞれのドキュメントで示されます。

8.9. 複数の関連しない例外の送出と処理

いくつか発生した例外の報告が必要な状況があります。並列処理のフレームワークでは、複数のタスクが平行して失敗することがたびたびあります。他にも最初の例外を送出するよりも、処理を継続して複数の例外を集約した方が望ましいユースケースもあります。

組み込みの ExceptionGroup は例外インスタンスのリストをまとめ、同時に送出できるようにします。ExceptionGroupも例外なので、他の例外と同じように捕捉できます。:

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

except の代わりに except* を使用すると、グループの中にある特定の型に一致した例外だけを選択して処理できます。以下の例では、ネストした例外グループに対して各 except* 節で特定の型の例外を取り出し、それ以外の例外は他の節に伝えられ、最終的に例外が送出されます。:

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |     ...<12 lines>...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

例外グループの中に含める例外は、型ではなくインスタンスである必要があることに注意してください。これは一般的に、以下のパターンようにプログラムで送出された複数の例外を捕捉することが多いためです。:

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

8.10. ノートによって例外を充実させる

例外を送出するために生成するときに、通常は発生したエラーを説明する情報で初期化されます。例外を受け取ったあとに情報を追加すると便利な場合があります。この目的のために、例外は add_note(note) メソッドを持ちます。このメソッドは文字列を受け取り、例外のノートのリストに追加します。標準のトレースバックでは例外の後に、全てのノートが追加した順番に出力されます。:

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

たとえば、複数の例外を1つの例外グループにまとめるときに、各エラーのコンテキスト情報を追加したい場合があります。以下のグループ内の各例外が持つノートは、エラーがいつ発生したかを示しています。:

>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>