【第9回】例外処理入門:エラーとやさしく付き合う try / except

Python入門

ここまでのシリーズで、

  • 変数・リスト・辞書で「データ」を扱い
  • if / for / while で「流れ」と「くり返し」を扱い
  • CSVファイルにコーヒーや釣りのログを保存し
  • 関数で「よく使う処理」に名前をつけて整理する

ところまで進んできました。

第7回・第8回のコードの中では、さりげなく

try:
    score = float(score_str)
except ValueError:
    print("数値に変換できなかったので、スコアを 3.0 にします。")
    score = 3.0

これは、

「ユーザーの入力がちょっとおかしくても、スクリプト全体が落ちないようにするクッション」

のようなものです。

ただ、本気で “落ちないツール” に育てていくなら、

  • 例外(Exception)とはそもそも何なのか
  • try / except をどこに書くべきか
  • どこまでを「許容される入力ミス」と見なし、どこからを「止めるべきエラー」と見るか

といったことを、一度ちゃんと整理しておいた方が安心です。

今回のテーマは、

「エラーとやさしく付き合うための、Pythonの例外処理の基本」を押さえること。

湖での釣りやコーヒーの時間と同じように、
予期せぬ風や雨(エラー)があっても、慌てずに付き合えるようになっていきましょう。


この回のゴールと全体像

第9回で目指すゴールは、次の4つです。

  1. 例外(Exception)とは何か、「エラー」との関係がざっくり分かる
  2. try / except の基本形と、よくある例外(ValueError, FileNotFoundErrorなど)がイメージできる
  3. コーヒー&釣りログツールに「落ちないためのクッション」を入れられる
  4. 「関数の中で例外を扱って、呼び出し側をスッキリさせる」感覚をつかむ

その上で、

  • 次回の クラス超入門(CoffeeLog / FishingLogクラス)

に繋げていきます。

例外(Exception)とは何か?

まずはイメージから。

Pythonで、こんなコードを書いたとします。

x = int("abc")  # 数字じゃない文字列を整数に変換しようとする

実行すると、Pythonはこんなエラーメッセージを出して止まります。

ValueError: invalid literal for int() with base 10: 'abc'

この「ValueError」が 例外(Exception) の一種です。

ざっくりいうと、

「Pythonが ‘これはもう普通には続けられない…’ と判断した状態を、例外という形で知らせてくる」

と考えてOKです。

代表的なものだけ挙げておくと:

  • ValueError:値の内容が不正(数値に変換できない文字列など)
  • TypeError:型の組み合わせがおかしい(文字列+数値を足すなど)
  • ZeroDivisionError:0で割ろうとした
  • FileNotFoundError:指定したファイルが存在しない

こうした「例外」が発生すると、何もしなければプログラムはそこで止まります。

try / except の基本形 – 「ダイブしてみて、ダメなら別ルート」

例外が発生する可能性があるコードに対して、

「とりあえずやってみて、失敗したらこうしてね」

とPythonに伝えるのが try / except です。

基本形

try:
    あやしいかもしれない処理
except 例外の種類:
    失敗したときにやってほしい処理

例:0で割ったときにメッセージを出して続行する

try:
    result = 10 / 0
    print("結果:", result)
except ZeroDivisionError:
    print("0で割ることはできません。計算をスキップします。")

通常なら ZeroDivisionError で止まってしまうところを、

  • エラーメッセージを出しつつ
  • スクリプト全体は落とさずに続ける

という振る舞いに変えています。

コーヒースコアの変換に try / except を使う(復習+整理)

第7回・第8回で出てきた、コーヒースコアの変換部分をもう一度見てみます。

score_str = input("スコア(0〜5の数字):")

try:
    score = float(score_str)
except ValueError:
    print("数値に変換できなかったので、スコアを 3.0 にします。")
    score = 3.0

ここで起きうる問題は、

  • ユーザーが「4.5」ではなく「美味しい」と打ってしまう
  • 何も入力せずにEnterを押してしまう

などです。

こうしたとき float(score_str)ValueError を投げますが、

  • その場でキャッチして
  • 「じゃあデフォルトの 3.0 にしよう」

という判断を、この関数の中だけで完結 させています。

これにより、

  • 呼び出し側は「常に score は float だ」という前提で後続処理を書ける

という状態になります。

例外処理の“粒度”を考える – どこに try / except を書くか?

例外処理でよくある“失敗パターン”のひとつに、

try:
    この中にいろんな処理を書きまくる
    ファイルも開いて
    CSVも読み込んで
    ログも表示して
    // 何かエラーが起きるかも…
except Exception:
    とりあえず「エラーが起きました」とだけ表示する

という 「雑に全部を try に突っ込む」 形があります。

これをやると、

  • どこで何のエラーが起きたのか分かりにくい
  • 本来は止めるべき致命的なエラーまで握りつぶしてしまう

という問題が出てきます。

基本的には、

「エラーになりそうな1か所〜数か所だけ」を try に入れ、失敗したときの振る舞いをしっかり決めておく

ほうが安全です。

悪い例(広すぎる try)

try:
    # 何十行も処理がある
    # ファイルも開くし、型変換もするし、計算もする
    ...
except Exception:
    print("エラーが起きました")

良い方向の例(狭める)

# ファイルを開くところだけ try にする
try:
    with open("coffee_logs.csv", "r", encoding="utf-8") as f:
        ...
except FileNotFoundError:
    print("ログファイルがまだありません。最初の1回目かもしれません。")


# 数値変換のところだけを try にする
score_str = input("スコア(0〜5の数字):")
try:
    score = float(score_str)
except ValueError:
    print("数値に変換できなかったので、スコアを 3.0 にします。")
    score = 3.0

こんなふうに、

  • 「どの部分で何が起きるか分からない」

のではなく、

  • 「ここではファイルがないかもしれない」
  • 「ここでは数値に変換できないかもしれない」

予想を具体的に書いておく のが理想です。

複数の例外を分けて扱う – FileNotFoundError と ValueError

コーヒーログを読み込んで平均スコアを計算するようなスクリプトを考えてみます。

import csv

total_score = 0.0
count = 0

with open("coffee_logs.csv", "r", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        score = float(row["score"])
        total_score += score
        count += 1

ここで起きうる問題は、ざっくり2つあります。

  1. coffee_logs.csv というファイルがまだ存在しない → FileNotFoundError
  2. CSVの score が壊れている(文字列など) → ValueError

これを、例外ごとに分けて扱ってみます。

import csv

total_score = 0.0
count = 0

try:
    with open("coffee_logs.csv", "r", newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)

        for row in reader:
            try:
                score = float(row["score"])
            except ValueError:
                print("スコアが数値ではない行がありました。スキップします。")
                continue

            total_score += score
            count += 1

except FileNotFoundError:
    print("coffee_logs.csv がまだありません。先にコーヒーログを記録してください。")

if count > 0:
    average = total_score / count
    print("平均スコア:", round(average, 2))

ここでの考え方は:

  • 「ファイルが存在しない」のは、全体として処理できない状態 → 外側の try / except で対応
  • 「スコアが数字じゃない行が紛れ込んでいる」のは、その行だけスキップすればよい → 内側の try / except で対応

という切り分けです。

else / finally もあるけど、まずは try / except だけで十分

例外処理には、

  • try
  • except

に加えて、

  • else:例外が起きなかったときにだけ実行される
  • finally:例外が起きても起きなくても、最後に必ず実行される

という節もあります。

例としてはこんな形です。

try:
    f = open("coffee_logs.csv", "r", encoding="utf-8")
except FileNotFoundError:
    print("ファイルがありません。")
else:
    print("ファイルが開けました。中身を読み込みます。")
    # 読み込み処理...
finally:
    print("後片付きをします。")
    try:
        f.close()
    except NameError:
        # f がそもそも定義されていない場合
        pass

ただ、初心者のうちは無理に else / finally を使おうとしなくても大丈夫です。

  • 「ここは失敗する可能性がある」部分を try で囲み
  • 「その失敗をどう扱うか」を except に書く

という基本パターンに慣れてから、
else / finally に手を伸ばすくらいで十分です。

関数+例外処理の“役割わけ”を意識してみる

第8回で input_one_coffee_log()append_logs_to_csv() を関数化しました。
ここに例外処理も組み込んでいくときのポイントは:

「この関数の責務(役割)はどこまで?」

を意識することです。

入力用の関数では「入力ミス」を吸収する

def input_score(default=3.0):
    """スコアを入力させて float で返す。ダメなら default を返す。"""

    score_str = input("スコア(0〜5の数字):")

    try:
        return float(score_str)
    except ValueError:
        print(f"数値に変換できなかったので、スコアを {default} にします。")
        return default
def input_one_coffee_log():
    name = input("コーヒー名を入力してください(終了するには q):")
    if name == "q":
        return None

    roast = input("焙煎度(浅煎り・中煎り・深煎りなど):")
    memo = input("ひとことメモ:")
    score = input_score()

    return {
        "name": name,
        "roast": roast,
        "memo": memo,
        "score": score,
    }

こうすれば、「ユーザーの入力ミス」は input_score() の中だけで完結し、
呼び出し側は「スコアは必ず float」と信じて扱えます。

ファイルを開く関数では「ファイルがない」場合の方針を決める

たとえば、釣りログを読み込む関数を作るなら:

import csv
import os

def read_fishing_logs(filename):
    """釣りログCSVを読み込んで辞書のリストを返す。

    ファイルがない場合は、空のリストを返す。
    """

    if not os.path.exists(filename):
        print(f"{filename} がまだありません。空のログとして扱います。")
        return []

    logs = []

    with open(filename, "r", newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            logs.append(row)

    return logs

ここでは、

  • ファイルがない → 例外を投げるのではなく、「空のリスト」として扱う

という判断をしています。

逆に、

  • ファイルがないのは絶対におかしいので、呼び出し元で止めたい

という場合は、FileNotFoundError をそのまま外に投げてもOKです。

この「どこで例外をキャッチするか」「どこまで許すか」は、
ツールの性格や、自分がどう使いたいかで変わってきます。

例外を「自分で投げる」こともできる(raise)

もう一歩だけ踏み込むと、

条件に応じて、自分で例外を投げる(raise)

こともできます。

たとえば、スコアは0.0〜5.0の範囲にしておきたい、という場合:

def validate_score(score):
    if not (0.0 <= score <= 5.0):
        raise ValueError(f"スコアが範囲外です: {score}")
score = float(input("スコア(0〜5):"))
validate_score(score)

こうしておけば、「ありえないスコア」が入ったときには
あえて例外を投げて「気づけるようにする」という設計もできます。

次回扱う クラス(CoffeeLog / FishingLog) の中で、

  • コンストラクタ(__init__)や
  • 入力用メソッド

の中から raise するパターンもよく使います。

今日のまとめ 「落ちない」だけでなく「気づける」コードへ

今回は、

  • 例外(Exception)とは何か
  • try / except の基本形と、ValueError / FileNotFoundError などのよくある例外
  • コーヒー&釣りログツールにおける入力ミスやファイル有無の扱い方
  • 関数の中に例外処理を閉じ込めて、呼び出し側をスッキリさせる考え方
  • 場合によっては raise で「ありえない状態」をあえて表に出す

といった話をしてきました。

大事なのは、

  • ただ「落ちない」ようにするだけでなく
  • どこで、どんな異常が起きたときに、どう振る舞うべきか

を、少しずつ 自分のツールの文脈で決めていく ことです。

コーヒーも釣りも、予期せぬことが起きるから面白くて、
その中でも「自分なりのルール」を持って付き合っていきます。

例外処理も、それに少し似ています。
エラーと丁寧に付き合えるようになるほど、
あなたのPythonツールは、現実の世界に馴染んでいきます。

次回予告 クラス超入門:CoffeeLog / FishingLog を「ひとつの存在」にする

関数と例外処理までたどり着いたので、
いよいよ次回からは クラス(class) に入っていけます。

ここまでの流れを振り返ると:

  • 辞書で「1件分のログ」を表現し
  • 関数で「そのログに対する処理」に名前をつけ
  • 例外処理で「おかしな状態」をケアし始めた

という状態です。

次のステップは、

「データ(状態)と、それに関する処理(メソッド)をひとつにまとめた“存在”として扱う」

こと。

具体的には、

  • CoffeeLog クラス
    • name, roast, memo, score をプロパティとして持つ
    • to_dict() でCSV用の辞書に変換する
    • comment() でスコアに応じたコメントを返す
    • コンストラクタの中で validate_score() を呼んで、例外を投げることもできる
  • FishingLog クラス
    • date, lake, fish_type, fish_length_cm, is_released などを持つ
    • 「50アップかどうか」を判定するメソッドを持たせる

などを題材に、

「クラス=データと処理をまとめた小さな宇宙」

として眺めていきます。

朝のコーヒー、湖でのブラウン、夜の星空。
それぞれのログを、
Pythonの世界では CoffeeLogFishingLog という
“ひとつの存在”として扱えるようにしていきましょう。

コメント