Python | 1 日 120 分 × 7 日アプリ学習:エラーハンドリング付き入力アプリ(中級編)

Web APP Python
スポンサーリンク

4日目のゴール

4日目のテーマは
「try / except を“設計レベル”で使い、エラー処理をコードの一部としてきれいに組み込む」 ことです。

ここまでであなたはすでに、

入力エラーを落とさず処理できる
安全な入力関数を作れる
ファイル読み書きに try / except を使える

というところまで来ています。

4日目では一歩進んで、

エラー処理の「責任範囲」をはっきり分ける
「ここでは例外を握りつぶさない」という判断を学ぶ
アプリ全体の流れとエラー処理をきれいに共存させる

という、“中級者のエラーハンドリングの感覚” を育てます。


エラー処理には「3つのレベル」があると考える

レベル1:その場で対処して終わるエラー

例えば、整数入力のときの ValueError

def input_int(prompt):
    while True:
        text = input(prompt)
        try:
            value = int(text)
        except ValueError:
            print("数字で入力してください。")
            continue
        return value
Python

このレベルのエラーは、

ユーザーにメッセージを出す
やり直させる
それで完結する

という「その場完結型」です。

ここでは、例外を上に投げる必要はありません。
try / except の中で完結させてOKです。


レベル2:その場では判断できないエラー(上に投げたい)

例えば、ファイル読み込み関数の中での OSError

def load_text(filename):
    with open(filename, "r", encoding="utf-8") as f:
        return f.read()
Python

ここで FileNotFoundErrorPermissionError が起きたとき、
「どうするか」は関数の中だけでは決めきれないことがあります。

空文字として扱うのか
「ファイルがないので新規作成します」とするのか
アプリ全体を終了させるのか

これは、アプリの設計レベルの判断です。

この場合は、あえてここでは try / except を書かず、
「例外を上に投げる」 という選択も大事です。


レベル3:アプリ全体の「最後の砦」としてのエラーハンドリング

一番外側、例えば main() の中で、

「ここまで来た例外は、もうアプリ全体として扱う」

という場所を作るのも、設計としてとても重要です。

def main():
    try:
        run_app()
    except Exception as e:
        print("予期しないエラーが発生しました。アプリを終了します。")
        print(f"詳細: {e}")
Python

ここは、
「本来は下の層で処理されるべきだったけど、漏れてきたエラー」を
最後に受け止める場所です。

4日目では、この「どのレベルで処理するか」を意識して
try / except を配置していきます。


「握りつぶす」と「投げる」を意識して選ぶ

なんでもかんでも except で終わらせると危険

悪い例をあえて出します。

def load_text(filename):
    try:
        with open(filename, "r", encoding="utf-8") as f:
            return f.read()
    except Exception:
        return ""
Python

一見「安全そう」に見えますが、
これはかなり危険です。

ファイルが存在しない
権限がない
ディスクが壊れている
パスの指定ミス
プログラミングミス(例えば filename が None)

全部まとめて「空文字にする」ことになります。

本来は気づくべきバグまで飲み込んでしまい、
「なぜかデータが空になるアプリ」になってしまいます。


「ここでは処理しない」という勇気を持つ

例えば、こういう分け方が健全です。

def load_text_raw(filename):
    with open(filename, "r", encoding="utf-8") as f:
        return f.read()
Python

この関数は、あえて try / except を書きません。
「ファイル読み込みに失敗したら例外を投げる関数」として定義します。

そして、使う側で方針を決めます。

def load_text_safe(filename):
    try:
        return load_text_raw(filename)
    except FileNotFoundError:
        print("ファイルがありません。新規として扱います。")
        return ""
    except PermissionError:
        print("ファイルを読む権限がありません。")
        return ""
Python

ここでの本質は、

「例外をどこで処理するか」を意識的に決める
「下の層では投げるだけ、上の層で方針を決める」という分業をする

という設計の考え方です。


入力アプリに「ビジネスルールのエラー」を足してみる

型エラーと「意味としておかしい」エラーを分ける

例えば、簡単な「予約人数入力アプリ」を考えます。

仕様はこうです。

人数は整数
1〜10人まで
それ以外はエラー

まずは素朴に書くとこうなります。

def input_people_count():
    while True:
        text = input("人数を入力してください(1〜10): ")

        try:
            value = int(text)
        except ValueError:
            print("数字で入力してください。\n")
            continue

        if not (1 <= value <= 10):
            print("1〜10の範囲で入力してください。\n")
            continue

        return value
Python

これはこれでOKです。
ただ、ここには二種類のエラーが混ざっています。

文字列 → 整数変換の失敗(技術的なエラー)
1〜10の範囲外(ビジネスルールのエラー)

この二つを意識して分けておくと、
アプリが大きくなったときに整理しやすくなります。


「意味としておかしい」エラーを例外として扱う

少しだけ踏み込んでみましょう。

class BusinessRuleError(Exception):
    pass

def validate_people_count(value):
    if not (1 <= value <= 10):
        raise BusinessRuleError("人数は1〜10の範囲でなければなりません。")
Python

そして、入力関数側ではこう使います。

def input_people_count():
    while True:
        text = input("人数を入力してください(1〜10): ")

        try:
            value = int(text)
            validate_people_count(value)
        except ValueError:
            print("数字で入力してください。\n")
            continue
        except BusinessRuleError as e:
            print(str(e) + "\n")
            continue

        return value
Python

ここでの重要ポイントは、

技術的なエラー(ValueError)と
ビジネスルールのエラー(BusinessRuleError)を
別の例外として扱っていることです。

これにより、

ログに残すときに区別できる
上の層で「どの種類のエラーか」を見て振る舞いを変えられる

という柔軟さが生まれます。

4日目の段階では「こういう分け方もあるんだ」と知っておけば十分です。


アプリ全体の「エラーの流れ」を設計する

小さな入力アプリでも「流れ」を意識してみる

次のようなアプリを考えます。

年齢を入力(1〜120)
月収を入力(0以上)
年収を計算して表示

ここに、try / except を設計的に組み込みます。

class BusinessRuleError(Exception):
    pass

def input_int(prompt):
    while True:
        text = input(prompt)
        try:
            value = int(text)
        except ValueError:
            print("数字で入力してください。\n")
            continue
        return value

def input_age():
    while True:
        age = input_int("年齢を入力してください(1〜120): ")
        try:
            if not (1 <= age <= 120):
                raise BusinessRuleError("年齢は1〜120の範囲で入力してください。")
        except BusinessRuleError as e:
            print(str(e) + "\n")
            continue
        return age

def input_monthly_income():
    while True:
        income = input_int("月収を入力してください(0以上): ")
        try:
            if income < 0:
                raise BusinessRuleError("月収は0以上で入力してください。")
        except BusinessRuleError as e:
            print(str(e) + "\n")
            continue
        return income

def run_app():
    age = input_age()
    income = input_monthly_income()
    yearly = income * 12
    print(f"\nあなたの年齢は {age} 歳、推定年収は {yearly} です。")

def main():
    try:
        run_app()
    except Exception as e:
        print("予期しないエラーが発生しました。アプリを終了します。")
        print(f"詳細: {e}")

main()
Python

ここでの構造を整理すると、

input_int は「技術的なエラー(ValueError)」だけを扱う
input_age / input_monthly_income は「意味としておかしい値」を扱う
run_app は「アプリの本来の処理」に集中する
main は「最後の砦」として、漏れてきた例外をまとめて扱う

という役割分担になっています。


4日目のミニアプリ:メニュー+入力+ファイル+エラー設計をまとめる

小さくても「設計されたエラーハンドリング」を体験する

仕様はこうです。

簡単な「支出記録アプリ」
メニューで「1: 支出を追加」「2: 合計を見る」「0: 終了」
金額は整数、0以上
ファイル expenses.txt に1行1金額で保存
ファイルがなくても落ちない
入力・ファイル・メニュー、それぞれに適切な try / except

コードの骨格はこんな感じになります。

import os

FILENAME = "expenses.txt"

class BusinessRuleError(Exception):
    pass

def input_int(prompt):
    while True:
        text = input(prompt)
        try:
            value = int(text)
        except ValueError:
            print("数字で入力してください。\n")
            continue
        return value

def input_amount():
    while True:
        amount = input_int("金額を入力してください(0以上): ")
        try:
            if amount < 0:
                raise BusinessRuleError("金額は0以上で入力してください。")
        except BusinessRuleError as e:
            print(str(e) + "\n")
            continue
        return amount

def append_expense(amount):
    try:
        with open(FILENAME, "a", encoding="utf-8") as f:
            f.write(str(amount) + "\n")
    except OSError:
        print("ファイルに書き込めませんでした。")

def load_expenses():
    if not os.path.exists(FILENAME):
        return []
    try:
        with open(FILENAME, "r", encoding="utf-8") as f:
            lines = f.readlines()
    except OSError:
        print("ファイルを読み込めませんでした。")
        return []
    expenses = []
    for line in lines:
        line = line.strip()
        if line == "":
            continue
        try:
            value = int(line)
        except ValueError:
            print(f"不正な行をスキップしました: {line}")
            continue
        expenses.append(value)
    return expenses

def run_app():
    while True:
        print("\n=== 支出記録アプリ ===")
        print("1: 支出を追加")
        print("2: 合計を見る")
        print("0: 終了")

        choice = input_int("番号を選んでください: ")

        if choice == 1:
            amount = input_amount()
            append_expense(amount)
            print("支出を記録しました。")
        elif choice == 2:
            expenses = load_expenses()
            total = sum(expenses)
            print(f"これまでの合計支出は {total} です。")
        elif choice == 0:
            print("終了します。")
            break
        else:
            print("その番号のメニューはありません。\n")

def main():
    try:
        run_app()
    except Exception as e:
        print("予期しないエラーが発生しました。アプリを終了します。")
        print(f"詳細: {e}")

main()
Python

ここまで来ると、

入力エラー
ビジネスルールエラー
ファイルエラー
予期しないエラー

それぞれに対して、
「どこで」「どう扱うか」がはっきり分かれています。


4日目で絶対に押さえてほしい本質

try / except は「設計の一部」になる

今日いちばん大事なのは、

try / except
「とりあえず落ちないようにするための道具」から
「どの層で何を責任を持つかを表現するための道具」

として捉え直すことです。

どこで例外を投げるか
どこで例外を捕まえるか
どの例外は握りつぶしてよくて
どの例外は上に伝えるべきか

これを意識して書き始めた瞬間、
あなたのコードはもう「中級者の設計」に入っています。

5日目以降は、この感覚を
JSON保存アプリやより複雑な入力フローに重ねていきます。
今日の支出アプリを、自分なりに機能追加しながら、
「このエラーはどこで扱うのが気持ちいいか」を探ってみてください。

タイトルとURLをコピーしました