一覧PG-10-1 · 例外とエラー処理

構文エラーと例外の伝播

compileとexecで解析と実行を分け、失敗前の記録が残るか確認します。次に関数の呼出しを二段にし、tracebackの各行と代入先の値を照合します。

説明

コードを実行可能な形へ解析するときの失敗と、値を処理している途中の失敗は起きる段階が違います。実行中の例外は通常の続きの処理を止め、処理できる呼出元へ伝わります。

compileとexecで解析と実行を分け、失敗前の記録が残るか確認します。次に関数の呼出しを二段にし、tracebackの各行と代入先の値を照合します。

1構文を解析する段階で失敗

sourceは後で実行する予定のコードです。ifのコロンがないため、compileで構文エラーになります。

events = []実行記録を初期化; source = 'events.append("before")\nif True print("bad")'解析するコード文字列; compile(source, "syntax_case.py", "exec")名前を付けて構文解析

SyntaxErrorが示すsyntax_case.pyの2行目はsourceの中の行です。compileはこのコードを解析するだけで、その先頭のevents.appendもまだ実行していません。

2解析だけでは副作用が起きない

記録を確認し、構文は正しいがゼロ除算を含むコードをcompileします。

print(events)構文エラー後の記録; source = 'events.append("before")\nresult = 1 / 0\nevents.append("after")'実行途中で失敗するコード; compiled = compile(source, "runtime_case.py", "exec")構文は解析できる; print(type(compiled).__name__)実行可能なコードの型

記録は空のままです。1/0も文法としては正しいため、今回はコードオブジェクトを作れます。具体的な値で割算が失敗するかは実行時の問題です。

3実行中の例外で後続が止まる

compile済みのコードをexecで実行します。

exec(compiled)解析済みコードを実行

ZeroDivisionErrorがruntime_case.pyの2行目を指します。最初の記録追加は実行済みで、割算より後のafterには進みません。

4失敗前の変更を確認

記録されたラベルを表示します。

print(events)実行できた範囲

beforeだけが残っています。例外によって、既に済んだリスト変更が自動で取り消されたわけではありません。構文解析の段階で止まった場合とは、残る状態が違います。

5入力を変換する関数

次は関数の呼出しを追います。parse_countは呼ばれた記録を残し、文字列を整数へ変換します。

def parse_count(text): events.append("parse")変換関数への到達記録 return int(text)変換結果を返す

変換できない文字列ならintでValueErrorになります。この関数は例外を捕捉する処理を持ちません。

6変換関数を呼び出す処理

per_itemは合計を個数で割り、一件当たりの値を求めます。変換後に進んだことも記録します。

def per_item(total, text): events.append("per_item")外側の関数への到達 count = parse_count(text)内側の変換を呼ぶ events.append("divide")変換後に到達する記録 return total / count一件当たりの計算

parse_countが成功した場合だけdivideまで進みます。呼出先から例外が伝わると、通常の戻り値を受け取る経路には進みません。

7呼出し列をtracebackで確認

answerへ以前の値99を置き、不正入力threeで呼びます。

events.clear(); answer = 99記録と以前の代入先; answer = per_item(12, "three")右辺の呼出しで失敗

tracebackには対話行、per_itemの呼出し、parse_countの変換という経路が出ます。最後のValueErrorが直接の失敗内容です。外側の関数も、正常なreturnへは進みません。

8代入前の値と到達範囲

answerと記録を確認します。

print(answer, events)代入先と実行済み処理

answerは99のままで、記録はper_itemとparseだけです。右辺が例外で終わったため、answerへの新しい代入とdivideの記録は行われていません。

9同じ経路を正常入力で実行

入力を3にして同じ関数を呼びます。

events.clear()比較用の記録を空にする; answer = per_item(12, "3")正常な入力で再実行; print(answer, events)戻り値と到達範囲

4.0が入り、divideまで記録されます。例外の末尾だけでなく、呼出し経路と前後の状態を照合すると、どの段階で止まったかを確認できます。

まとめ

構文解析の失敗と実行中の失敗では、実行済みの範囲が異なります。

tracebackの呼出し列と、例外前に残る状態を照合します。

操作履歴と解説を振り返れます。

Python 3

1
events = []; source = 'events.append("before")\nif True print("bad")'; compile(source, "syntax_case.py", "exec")
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "syntax_case.py", line 2
    if True print("bad")
            ^^^^^
SyntaxError: invalid syntax
2
print(events); source = 'events.append("before")\nresult = 1 / 0\nevents.append("after")'; compiled = compile(source, "runtime_case.py", "exec"); print(type(compiled).__name__)
[]
code
3
exec(compiled)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "runtime_case.py", line 2, in <module>
ZeroDivisionError: division by zero
4
print(events)
['before']
5
def parse_count(text): events.append("parse") return int(text)
6
def per_item(total, text): events.append("per_item") count = parse_count(text) events.append("divide") return total / count
7
events.clear(); answer = 99; answer = per_item(12, "three")
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "<console>", line 3, in per_item
  File "<console>", line 3, in parse_count
ValueError: invalid literal for int() with base 10: 'three'
8
print(answer, events)
99 ['per_item', 'parse']
9
events.clear(); answer = per_item(12, "3"); print(answer, events)
4.0 ['per_item', 'parse', 'divide']

演習完了