第9回:例外処理¶
前回までで、クラス・メソッド・継承を使って自前の型を組み立てられるようになった。take_damage の中に if self.hp < 0: という門番も置いて、HP がマイナスにならないよう守った。
それでもまだ守れていない種類のおかしな入力がある。たとえば a.take_damage('つよい') のように そもそも数字でない値が渡ってきたら、self.hp - 'つよい' で型エラーが出て、プログラムごと落ちる。今回はその対処方法、例外処理 を学ぶ。
1 か所のエラーで全部止まる¶
「変な値」と一口に言っても、実は 2 種類ある。
- ① エラーになって落ちるもの(例:
'つよい'を数字に変換しようとする)→ 今日の 例外処理 - ② エラーにはならないが論理的におかしいもの(例:
-50のダメージ → 回復してしまう)→ 前回の門番(if)
今日やるのは①。何が困るかというと、たった 1 か所のエラーで、後ろの処理まで全部巻き添えで止まってしまうこと。
a = Hero('勇者', 100)
damage = int(input('ダメージ量: ')) # ← ユーザが 'つよい' と打つと…
a.take_damage(damage)
print('戦闘を続けます') # ← ここに来られない!
ダメージ量: つよい
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'つよい'
「戦闘を続けます」は表示されない。本来なら次の処理に進めるはずなのに、入力 1 つの失敗で全部止まる。
例外とは¶
例外(exception): プログラム実行時に発生したトラブル(やそれを表すもの)。発生するとプログラムは途中で止まる。
print('魔王の全体攻撃! ダメージを分担します')
total = input('受けた合計ダメージ: ')
members = input('生きている仲間の人数: ')
each = float(total) / float(members)
print('1人あたり', each, 'のダメージ')
このコードは入力の仕方によって違う種類の例外を出す:
- 入力された値が数字でない場合 →
ValueError - 人数の入力値が
0の場合 →ZeroDivisionError
魔王の全体攻撃! ダメージを分担します
受けた合計ダメージ: つよい
生きている仲間の人数: 4
Traceback (most recent call last):
File "battle.py", line 4, in <module>
each = float(total) / float(members)
ValueError: could not convert string to float: 'つよい'
try〜except 文¶
例外を捕まえて、止まらずに別の処理に切り替える構文が try〜except。
さっきの全体攻撃コードに例外処理を追加すると:
print('魔王の全体攻撃! ダメージを分担します')
try:
total = input('受けた合計ダメージ: ')
members = input('生きている仲間の人数: ')
each = float(total) / float(members)
print('1人あたり', each, 'のダメージ')
except:
print('ダメージを計算できませんでした')
print('次のターンへ')
これで 'つよい' や 0 が来ても、プログラムは落ちずに「次のターンへ」まで進む。
try〜except の流れ¶
- 例外なし:
tryを最後まで実行 →exceptは飛ばす → 後続へ - 例外あり:
tryを途中で中断 →exceptを実行 → 後続へ
どちらでもプログラムは止まらず、後続に進むのが try〜except の効果。
プレ演習9-1¶
次のコードを打ち込んで 2 回実行しよう。両方で最後の「戦闘を続けます」が表示されることを確認しよう。
print('ダメージ計算')
try:
damage = int(input('ダメージ量: '))
print('ダメージは', damage)
except:
print('数字を入力してください')
print('戦闘を続けます')
- (1) ダメージ量に
50を入力 - (2) ダメージ量に
つよいを入力
期待される実行結果:
例外の種類による処理の切り替え¶
except は例外の種類ごとに書ける。種類によって違うメッセージを出したり、違う処理に切り替えたりできる。
さっきの全体攻撃コードを、ValueError(数字でない)と ZeroDivisionError(人数 0)で別メッセージにすると:
print('魔王の全体攻撃! ダメージを分担します')
try:
total = input('受けた合計ダメージ: ')
members = input('生きている仲間の人数: ')
each = float(total) / float(members)
print('1人あたり', each, 'のダメージ')
except ValueError:
print('ダメージは数字で入力してください')
except ZeroDivisionError:
print('生きている仲間がいません')
print('次のターンへ')
「何が起きたか」によって違うメッセージを出すことで、ユーザにもデバッグ時の自分にも親切になる。
プレ演習9-2¶
次のコードを実行する前に、level に 5 を入れた場合と abc を入れた場合の出力を、紙やメモ帳に書き出そう。その後に実行して、自分の予測と合っているか確認しよう。
try:
level = int(input('レベル: '))
print('攻撃力は', level * 10)
except ValueError:
print('数字を入力してください')
else:
print('ステータス確定!')
finally:
print('--- 戦闘開始 ---')
ポイント
elseとfinallyの発火条件はそれぞれ違う。「例外が出たとき」「出なかったとき」「どちらでも」のうちどれに該当するか、本文の説明を見直そう。- 今回
levelに5を入れたとき例外は出るか?abcのときは?
プレ演習9-3¶
次のコードは、人数が 0 のとき(ZeroDivisionError)は処理してあるが、ある入力ではまだ落ちる。どんな入力で落ちるかを考え、それを捕まえる except を 1 つ追加して正しく動くようにしよう。
print('ゴールドを山分けします')
try:
gold = int(input('獲得ゴールド: '))
members = int(input('パーティの人数: '))
print('1人あたり', gold / members, 'ゴールド')
except ZeroDivisionError:
print('パーティに誰もいません')
ヒント
int(input(...)) が失敗するのはどんなときか? そのとき出る例外の名前は本文を見直す。
as:エラーの中身を受け取る¶
except 例外の種類 as 変数: と書くと、エラーの詳細メッセージを受け取れる。デバッグや、何が起きたかを正確にユーザに伝えたいときに便利。
try:
damage = int(input('ダメージ量: '))
print('ダメージは', damage)
except ValueError as e:
print('エラー内容:', e)
e には例外オブジェクトが入っていて、print(e) するとエラーの詳細メッセージが出る。
try〜except〜else〜finally¶
try〜except には、後ろにさらに 2 つの節を付けられる。
try:
例外が発生するかもしれない処理(1)
except:
例外が発生したときの処理(2)
else:
例外が発生しなかったときの処理(3)
finally:
例外の有無にかかわらず実行される処理(4)
実行順:
- 通常: 処理1 → 処理3(
else)→ 処理4(finally) - 例外時: 処理1(中断)→ 処理2(
except)→ 処理4(finally)
else は例外が出なかったときだけ、finally は両方で必ず実行される。finally は「後片付け」に使うのが定番(ファイルを閉じる、接続を切るなど)。
プレ演習9-4¶
次のコードには 3 か所の誤り がある。それぞれ見つけて、正しく動くように直そう。
party = ['勇者', 'マーリン', '戦士']
try
i = int(input('何番目のキャラ?: '))
print(party[i])
except ValuError:
print('数字を入力してください')
except IndexError:
print('そのキャラはいません')
ヒント
tryの行末は何が必要だった?(本文の構文を見直す)- 例外の種類の名前は本当に合っている?(本文で出てきたのは
ValueError) exceptブロックの中身のインデントは合っている?
プレ演習9-5¶
目的地まであと何ターンかかるかを計算するプログラムを try〜except で ゼロから書こう。
要件:
- 目的地までの距離 と 1 ターンの移動距離(速さ) をキーボードから入力する
- 距離 ÷ 速さ を
あと <ターン数> ターンの形で表示する - 数字以外が入力されたら
数字を入力してくださいと表示する - 速さが
0だったら移動できません(速さが0です)と表示する
期待される実行結果:
ヒント
- 2 種類の例外を扱うので、本文の「例外の種類による処理の切り替え」の形を使う
- どの操作で
ValueErrorが出るか / どの操作でZeroDivisionErrorが出るかを最初に考える
授業内演習¶
次のプログラムを実行し、正常動作する場合とそうでない場合の動作確認をしなさい。
try:
count = int(input("リストの要素数を入力: "))
number_list = []
for i in range(count):
n = int(input(f"{i + 1}番目の数字を入力してください: "))
number_list.append(n)
max_value = max(number_list)
print(f"最大値は {max_value} です。")
except ValueError as ve:
print(f"エラー: {ve}")
else:
print("正常に処理が完了しました。")
finally:
print("プログラムを終了します。")
確認の観点:
- 数字を順に入れて正常終了する流れ →
elseとfinallyのどちらが出力されるか - どこかで数字以外を入力した流れ →
exceptとfinallyのどちらが出力されるか veに入っているエラーメッセージは、入力した値によってどう変わるか
基本課題¶
キーボードから 2 つの整数を入力して、実行例のように出力するプログラムを try〜except 文を用いて作成しなさい。
実行例(正常時):
実行例(ゼロで割ったとき):
約束ごと:
try〜except文を必ず使うこと- ゼロ除算は
ZeroDivisionErrorをexceptで捕まえること - 結果の表示は
結果は: <値>の形に合わせること
応用課題¶
キーボード入力によりリストを初期化した後、リストのインデックスを入力し、実行例の通りに出力するプログラムを try〜except 文を用いて作成しなさい。
実行例(正常時):
リストのデータ数を入力してください ==> 5
data[0] = 3
data[1] = 2
data[2] = 1
data[3] = -5
data[4] = 4
リストのインデックスを入力してください: 3
インデックス 3 の値: -5.0
実行例(インデックスが範囲外):
リストのデータ数を入力してください ==> 5
data[0] = 3
data[1] = 2
data[2] = 1
data[3] = -5
data[4] = 4
リストのインデックスを入力してください: 5
エラー: インデックス 5 は範囲外です。
実行例(数値でない入力):
リストのデータ数を入力してください ==> 5
data[0] = 3
data[1] = 2
data[2] = 1
data[3] = -5
data[4] = 4
リストのインデックスを入力してください: a
エラー: 数値を入力してください。
約束ごと:
- リストのインデックスエラーに関する例外の種類には
IndexErrorを用いること - 数値でない入力は
ValueErrorで捕まえること - 出力メッセージは実行例のとおりに揃えること(
エラー: インデックス <i> は範囲外です。/エラー: 数値を入力してください。)
ヒント:
- リストの初期化部分は
forループ +inputで書く - インデックス入力で
int()するときと、list[i]でアクセスするときで、それぞれ別の例外が出る