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
パーサが構文解析エラーと判断した行が表示されるとともに、エラーが検出された部分に矢印 (^) が表示されます。ただし、この部分が本当に修正すべき箇所とは限らないので注意してください。上記の例では、エラーは関数 print() のところで検出されており、これはその前にコロン (':') が抜けているためです。
ファイルに書かれたコードの場合、ファイル名 (上の例では <stdin>) と行番号が表示されるので、どこを見ればよいかが分かるようになっています。
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("数字を入力してください: "))
... break
... except ValueError:
... print("おっと!それは数字ではないようです。もう一度入力してください...")
...
try 文は下記のように動作します。
何も例外が発生しなければ、 except 節 をスキップして
try文の実行を終えます。try節内の実行中に例外が発生すると、その節の残りは飛ばされます。次に、例外型がexceptキーワードの後に指定されている例外に一致する場合、except 節が実行された後、 try/except ブロックの後ろへ実行が継続されます。もしも except 節 で指定された例外と一致しない例外が発生すると、その例外は
try文の外側に渡されます。例外に対するハンドラ (handler、処理部) がどこにもなければ、 処理されない例外 (unhandled exception) となり、エラーメッセージを出して実行を停止します。
try 文には、異なる例外に対するハンドラを指定するために、複数の except 節 を設けることができます。
実行されるハンドラは最大で 1 つです。
ハンドラが処理するのは対応する 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)) # 例外の型
... print(inst.args) # 引数は .args に格納される
... print(inst) # __str__ は args を直接出力しますが、
... # 例外のサブクラスで上書きされる場合もあります
... x, y = inst.args # 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エラー:", err)
except ValueError:
print("データを整数に変換できません。")
except Exception as err:
print(f"予期せぬエラー {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(arg, 'が開けません')
else:
print(arg, 'は', len(f.readlines()), '行です')
f.close()
追加のコードを付け加えるのは try 節よりも else 節の方がよいでしょう。
なぜなら、そうすることで try ... except 文で保護されたコードから送出されたもの以外の例外を過って捕捉してしまうという事態を避けられるからです。
例外ハンドラは、try 節 の直接内側で発生した例外を処理するだけではなくその try 節 から (たとえ間接的にでも) 呼び出された関数の内部で発生した例外も処理します。例えば:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('実行時エラーを処理:', err)
...
実行時エラーを処理: 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 # 'raise ValueError()' の省略記法
例外が発生したかどうかを判定したいだけで、その例外を処理するつもりがなければ、単純な形式の raise 文を使って例外を再送出させることができます:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('例外が通り過ぎました!')
... raise
...
例外が通り過ぎました!
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 は例外インスタンスか 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節がbreak、continue、return文を実行すると、例外は再送出されません。 これは混乱を招く可能性があるため、推奨されません。 バージョン 3.14 から、コンパイラはこのようなコードに対してSyntaxWarningを出します (PEP 765 を参照)。もし
try文がbreak文、continue文またはreturn文のいずれかに達すると、そのbreak文、continue文またはreturn文の実行の直前にfinally節が実行されます。finally節にreturn文がある場合、try節のreturn文の値ではなく、finally節のreturn文の値が返されます。 これは混乱を招く可能性があるため、推奨されません。 バージョン 3.14 から、コンパイラはこのようなコードに対してSyntaxWarningを出します (PEP 765 を参照)。
例えば:
>>> 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 節はどの場合にも実行されています。
文字列で割り算をすることで発生した TypeError は except 節で処理されていないので、 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.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
+------------------------------------
>>>