構文エラーと例外の伝播
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
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 syntaxprint(events); source = 'events.append("before")\nresult = 1 / 0\nevents.append("after")'; compiled = compile(source, "runtime_case.py", "exec"); print(type(compiled).__name__)[] code
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
print(events)['before']
def parse_count(text):
events.append("parse")
return int(text)def per_item(total, text):
events.append("per_item")
count = parse_count(text)
events.append("divide")
return total / countevents.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'
print(answer, events)99 ['per_item', 'parse']
events.clear(); answer = per_item(12, "3"); print(answer, events)4.0 ['per_item', 'parse', 'divide']
演習完了