コンテンツにスキップ

第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

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) ダメージ量に つよい を入力

期待される実行結果:

(1) ダメージは 50
    戦闘を続けます

(2) 数字を入力してください
    戦闘を続けます

例外の種類による処理の切り替え

except例外の種類ごとに書ける。種類によって違うメッセージを出したり、違う処理に切り替えたりできる。

try:
    本来実行したい処理
except 例外の種類1:
    種類1 が発生したときの処理
except 例外の種類2:
    種類2 が発生したときの処理

さっきの全体攻撃コードを、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

次のコードを実行する前に、level5 を入れた場合と abc を入れた場合の出力を、紙やメモ帳に書き出そう。その後に実行して、自分の予測と合っているか確認しよう。

try:
    level = int(input('レベル: '))
    print('攻撃力は', level * 10)
except ValueError:
    print('数字を入力してください')
else:
    print('ステータス確定!')
finally:
    print('--- 戦闘開始 ---')
ポイント
  • elsefinally の発火条件はそれぞれ違う。「例外が出たとき」「出なかったとき」「どちらでも」のうちどれに該当するか、本文の説明を見直そう。
  • 今回 level5 を入れたとき例外は出るか? 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)
ダメージ量: つよい
エラー内容: invalid literal for int() with base 10: 'つよい'

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です) と表示する

期待される実行結果:

距離 100 / 速さ 20  →  あと 5.0 ターン
距離 abc            →  数字を入力してください
速さ 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("プログラムを終了します。")

確認の観点:

  • 数字を順に入れて正常終了する流れ → elsefinally のどちらが出力されるか
  • どこかで数字以外を入力した流れ → exceptfinally のどちらが出力されるか
  • ve に入っているエラーメッセージは、入力した値によってどう変わるか

基本課題

キーボードから 2 つの整数を入力して、実行例のように出力するプログラムを try〜except 文を用いて作成しなさい。

実行例(正常時):

a/bを計算します。
aを入力してください: 4
bを入力してください: 2
結果は: 2

実行例(ゼロで割ったとき):

a/bを計算します。
aを入力してください: 4
bを入力してください: 0
ゼロで除算することはできません。

約束ごと:

  • try〜except 文を必ず使うこと
  • ゼロ除算は ZeroDivisionErrorexcept で捕まえること
  • 結果の表示は 結果は: <値> の形に合わせること

応用課題

キーボード入力によりリストを初期化した後、リストのインデックスを入力し、実行例の通りに出力するプログラムを 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] でアクセスするときで、それぞれ別の例外が出る