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

Web APP Python
スポンサーリンク

6日目のゴール

6日目のテーマは
「try / except を“アプリの流れ”と一体化させて、入力〜処理〜保存までを一気通貫で守る」 ことです。

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

入力エラーを防げる
リトライ回数を制御できる
ログを残せる
どこで例外を処理するかを設計できる

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

6日目では一歩進んで、

入力 → 計算 → 保存(ファイル)という一連の流れを
ひとつのアプリとして組み立て、その各ポイントに
「意味のある try / except」を配置していきます。


今日の題材:家計の「1件の支出」を安全に登録するアプリ

流れを先にイメージする

まず、アプリの流れを言葉で整理します。

金額を入力する(整数、0以上)
カテゴリを入力する(文字列、空は不可)
日付を入力する(任意。空なら今日の日付)
それらを1件の「支出データ」としてまとめる
ファイルに追記する(1行1レコード)

この流れの中に、

入力エラー
ビジネスルールエラー
ファイルエラー

がそれぞれ潜んでいます。

6日目のゴールは、
これらを「バラバラに try で囲う」のではなく、
「層ごとに責任を分けて 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_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
Python

ここでの重要ポイントは二つです。

一つ目は、input_intinput_amount の役割分担です。
input_int は「文字列 → 整数」の変換だけを担当し、
input_amount は「その整数が意味として正しいか」を担当しています。

二つ目は、「意味としておかしい」ことを
BusinessRuleError という別の例外で表現していることです。
これにより、「技術的なエラー」と「ルール違反」が
頭の中でちゃんと分かれてきます。


カテゴリ入力も同じ考え方で作る

カテゴリは文字列ですが、
「空文字はダメ」というルールを入れてみます。

def input_category():
    while True:
        category = input("カテゴリを入力してください(例: 食費, 交通費): ").strip()
        try:
            if category == "":
                raise BusinessRuleError("カテゴリは空にできません。")
        except BusinessRuleError as e:
            print(str(e) + "\n")
            continue
        return category
Python

ここでは try / except の中に
「1行だけ」しか入っていないのがポイントです。

try の中には「例外を起こすかもしれない行」だけを入れる
その後の処理は、例外がなかった前提で書く

というスタイルを、意識的に守っています。


ドメイン層(アプリの“意味”の部分)を作る

「支出データ」を1つの辞書として組み立てる

入力関数を組み合わせて、
1件の支出データを作る関数を書きます。

from datetime import datetime

def create_expense():
    amount = input_amount()
    category = input_category()
    date_text = input("日付を入力してください(未入力なら今日の日付): ").strip()

    if date_text == "":
        date_str = datetime.now().strftime("%Y-%m-%d")
    else:
        date_str = date_text  # ここでは形式チェックは省略(後で拡張可能)

    expense = {
        "amount": amount,
        "category": category,
        "date": date_str,
    }

    return expense
Python

ここでのポイントは、

この関数の中には try / except が出てこないことです。

なぜかというと、

入力のエラーはすでに input_amount / input_category の中で
「ユーザーとの対話」として完結しているからです。

create_expense は、
「正しい値が返ってくる前提」で
アプリの“意味”に集中できます。

これが、「エラー処理を下の層に押し込める」 という設計です。


永続化層(保存)のエラーハンドリングを設計する

ファイルに1行で保存する関数を書く

支出データを、
amount,category,date のCSV風1行で保存してみます。

FILENAME = "expenses.csv"

def append_expense(expense):
    line = f"{expense['amount']},{expense['category']},{expense['date']}\n"
    try:
        with open(FILENAME, "a", encoding="utf-8") as f:
            f.write(line)
    except OSError as e:
        print("ファイルに書き込めませんでした。")
        log_error(f"OSError in append_expense: {e}")
        return False
    else:
        return True
Python

ここでの重要ポイントは三つです。

一つ目は、「保存に失敗した」という事実を
False という戻り値で表現していることです。
呼び出し側は、それを見て次の行動を決められます。

二つ目は、OSError をまとめて捕まえていること。
ファイル関連のエラーは多くの場合 OSError のサブクラスなので、
ここでは「ファイルに書けなかった」というレベルで十分です。

三つ目は、log_error を呼んで
詳細をログに残していることです。
ユーザーにはシンプルなメッセージ、
開発者(未来の自分)には詳細、という分担です。


アプリの「1回の登録フロー」を try / except で包む

入力 → 作成 → 保存をひとまとまりとして扱う

ここまでの部品を使って、
「1件の支出を登録する」フローを書きます。

def register_one_expense():
    try:
        expense = create_expense()
    except Exception as e:
        print("入力中に予期しないエラーが発生しました。登録を中止します。")
        log_error(f"Unexpected error in create_expense: {e}")
        return

    success = append_expense(expense)
    if not success:
        print("支出の保存に失敗しました。")
        return

    print("支出を登録しました。")
Python

ここでの深掘りポイントは二つです。

一つ目は、create_expense の呼び出しを
try / except で包んでいること。
本来は input_amount / input_category の中で
エラー処理が完結しているはずですが、
「もしも漏れてきたら」という最後の保険として
ここでまとめて捕まえています。

二つ目は、append_expense の戻り値を見て
「保存に失敗したときのメッセージ」を出していること。
ここでは例外ではなく、
「業務としての失敗」 を戻り値で表現しています。


メインループに「エラーに強いメニュー」を組み込む

メニュー選択も、もう一度きれいに整理する

最後に、メニュー付きのアプリにします。

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

def run_app():
    while True:
        print("\n=== 支出登録アプリ ===")
        print("1: 支出を1件登録")
        print("0: 終了")

        choice = input_menu_number()

        if choice == 1:
            register_one_expense()
        elif choice == 0:
            print("終了します。")
            break
        else:
            print("その番号のメニューはありません。\n")
Python

そして、一番外側に「最後の砦」を置きます。

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

main()
Python

これで、

入力層(ユーザーとの対話)
ドメイン層(支出データの意味)
永続化層(ファイル保存)
アプリ全体の最後の砦

それぞれに、
役割に合った try / except が配置された状態になります。


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

try / except を「流れの中で配置する」感覚

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

try / except
単発のテクニックとしてではなく、

入力 → 処理 → 保存 → 全体

というアプリの流れの中で、
どの層がどのエラーを責任を持って扱うか、
という“設計の一部”として考え始めることです。

入力層では「ユーザーとの対話」に集中する
ドメイン層では「意味のあるデータ」を前提にする
保存層では「外部との境界」を守る
一番外側では「予期しないエラー」を静かに受け止める

この構造を意識できるようになると、
あなたのアプリは一気に「現場で通用するコード」に近づきます。

7日目は、このエラーハンドリング付き入力の集大成として、
「小さくても本気で使えるアプリ」を形にしていきましょう。

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