Python | 1 日 120 分 × 7 日アプリ学習:CSVファイル読み書きアプリ(中級編)

Web APP Python
スポンサーリンク

6日目のゴール

6日目のテーマは
「5日目で作った“CSVユーティリティアプリ”を、少しだけ“設計の良いコード”に近づけること」 です。

ここまでであなたは、
ひとつのファイルの中に

CSV読み込み関数
集計・フィルタ・エラー分離の関数
メニューと main

を詰め込んだ「動くアプリ」を作りました。

6日目では、ここから一歩進んで、

共通処理を“ちゃんと共通化”する
「設定値」と「ロジック」を分ける
コードを読みやすくするための小さなリファクタリング

を体験してもらいます。

「動く」から「読みやすい・直しやすい」へ、半歩進める日です。


まずは「今のアプリの課題」を言葉にしてみる

動くけど、ちょっとゴチャっとしてきた状態

5日目のアプリを思い出してみると、こんな感じでした。

ファイル名 employees_dirty.csv が、あちこちにベタ書きされている
年齢や給与の数値変換ロジックが、複数の関数に重複している
「エラー行の条件」が関数ごとにバラバラに書かれている

動くことは動くけれど、
「あとから機能を足したり、仕様を変えたりするときに、どこを直せばいいか分かりにくい」
という状態になりつつあります。

6日目は、ここを少し整理します。


共通の「数値変換ヘルパー」を作る

int() を直接呼ぶのをやめてみる

今のコードでは、あちこちでこう書いていました。

age = int(row["age"])
salary = int(row["salary"])
Python

これを毎回 try / except で囲んでいる関数もありました。

ここを「共通の小さな関数」にしてしまうと、
コードがかなりスッキリします。

def to_int_or_none(text):
    if text is None:
        return None

    text = str(text).strip()
    if text == "":
        return None

    try:
        return int(text)
    except ValueError:
        return None
Python

この関数の意味を、丁寧に言葉にするとこうです。

引数を文字列として受け取り、前後の空白を削る
空文字列なら None を返す
int() に変換できれば整数を返す
変換できなければ None を返す

つまり、

「数値として使えないものは全部 None にしてしまう」
というヘルパーです。

これを使うと、例えばこう書けます。

age = to_int_or_none(row["age"])
salary = to_int_or_none(row["salary"])

if age is None or salary is None:
    # この行は数値として扱えない
    ...
Python

「数値変換のルール」が一箇所にまとまるので、
後から仕様を変えたいときも、この関数だけ見れば済みます。


「行が有効かどうか」を判定する関数を作る

エラー条件をバラバラに書かない

5日目のコードでは、
関数ごとに「エラー行の条件」がバラバラに書かれていました。

例えば、

給与が空欄ならスキップ
給与が数値でなければスキップ
年齢と給与が両方とも数値でなければエラー行

などです。

これを「行の妥当性をチェックする関数」としてまとめてみます。

def is_valid_employee_row(row):
    age = to_int_or_none(row.get("age"))
    salary = to_int_or_none(row.get("salary"))
    department = row.get("department", "").strip()

    if age is None:
        return False

    if salary is None:
        return False

    if department == "":
        return False

    return True
Python

この関数は、

年齢が数値として解釈できないなら False
給与が数値として解釈できないなら False
部署が空欄なら False
それ以外は True

というルールで「有効な行かどうか」を判定します。

ここで大事なのは、

「何を“有効な行”とみなすか」が一箇所にまとまっている

ということです。

これを使えば、例えば集計関数はこう書けます。

def show_salary_by_department(rows):
    if not rows:
        print("データがありません。")
        return

    stats = {}
    skipped = 0

    for row in rows:
        if not is_valid_employee_row(row):
            skipped += 1
            continue

        dept = row["department"].strip()
        salary = to_int_or_none(row["salary"])

        if dept not in stats:
            stats[dept] = {"total_salary": 0, "count": 0}

        stats[dept]["total_salary"] += salary
        stats[dept]["count"] += 1

    ...
Python

「この行を使うかどうか」の判断は
is_valid_employee_row に丸投げできるので、
集計ロジックがとても読みやすくなります。


設定値(ファイル名など)をまとめる

ベタ書きの “employees_dirty.csv” をやめる

5日目のコードでは、
ファイル名があちこちにベタ書きされていました。

DATA_FILE = "employees_dirty.csv"
...
export_over_30(rows, "employees_over30.csv")
export_error_rows(rows, "employees_error.csv")
Python

これを「設定セクション」としてまとめておくと、
後からファイル名を変えたいときに楽になります。

INPUT_FILE = "employees_dirty.csv"
OUTPUT_OVER30_FILE = "employees_over30.csv"
OUTPUT_ERROR_FILE = "employees_error.csv"
Python

こうしておけば、
main の中はこう書けます。

def main():
    rows = load_employees(INPUT_FILE)
    if not rows:
        print("処理を終了します。")
        return

    while True:
        show_menu()
        choice = input("番号を選んでください: ").strip()

        if choice == "1":
            show_all_employees(rows)
        elif choice == "2":
            show_salary_by_department(rows)
        elif choice == "3":
            export_over_30(rows, OUTPUT_OVER30_FILE)
        elif choice == "4":
            export_error_rows(rows, OUTPUT_ERROR_FILE)
        elif choice == "0":
            print("アプリを終了します。")
            break
        else:
            print("0〜4 の番号を入力してください。")
Python

「どのファイルを使っているか」が、
コードの上の方を見るだけで分かるようになります。


エラー行の抽出ロジックを共通化する

「エラー行だけ出したい」機能を、きれいに書き直す

4日目・5日目では、
エラー行の条件を関数の中に直接書いていました。

6日目では、
さっき作った is_valid_employee_row を使って
もっとシンプルに書き直してみます。

def collect_error_rows(rows):
    error_rows = []

    for row in rows:
        if not is_valid_employee_row(row):
            error_rows.append(row)

    return error_rows
Python

これだけです。

「何がエラーか」は is_valid_employee_row が知っているので、
ここでは「有効じゃない行を集める」だけに集中できます。

これを使って、
エラー行を書き出す関数はこうなります。

import csv


def export_error_rows(rows, output_file):
    if not rows:
        print("データがありません。")
        return

    error_rows = collect_error_rows(rows)

    if not error_rows:
        print("エラー行はありませんでした。")
        return

    fieldnames = rows[0].keys()

    with open(output_file, "w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        for row in error_rows:
            writer.writerow(row)

    print(f"{output_file} にエラー行を書き出しました。件数: {len(error_rows)} 件")
Python

「エラー行の定義」と「エラー行の書き出し」が
きれいに分離されているのが分かると思います。


「30歳以上の抽出」もヘルパーで表現する

条件を関数にしておくと、後から変えやすい

同じ発想で、
「30歳以上かどうか」を判定する関数も作ってみます。

def is_over_30(row):
    age = to_int_or_none(row.get("age"))
    if age is None:
        return False
    return age >= 30
Python

これを使えば、
30歳以上の抽出はこう書けます。

import csv


def export_over_30(rows, output_file):
    if not rows:
        print("データがありません。")
        return

    filtered = [row for row in rows if is_over_30(row)]

    if not filtered:
        print("30歳以上のデータがありませんでした。")
        return

    fieldnames = rows[0].keys()

    with open(output_file, "w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        for row in filtered:
            writer.writerow(row)

    print(f"{output_file} に 30歳以上のデータを書き出しました。件数: {len(filtered)} 件")
Python

「30歳以上」という条件を変えたくなったら、
is_over_30 だけを直せばいい、という状態になります。

例えば、「35歳以上」にしたくなったら、
この1行を変えるだけです。

return age >= 35
Python

6日目のまとめ 今日つかんでほしい感覚

今日の本質は、これです。

int() などの変換ロジックは「小さなヘルパー関数」にまとめると、あちこちで同じことを書かなくて済む。
「有効な行かどうか」「エラー行かどうか」といった判定は、専用の関数にしておくと、後から仕様を変えやすい。
ファイル名などの設定値は、コードの上の方にまとめておくと、アプリ全体の見通しがよくなる。
集計・フィルタ・エラー分離などの機能は、「条件を関数に切り出す」ことで、読みやすく・直しやすくなる。

5日目で「動くCSVアプリ」ができて、
6日目で「設計が少し整ったCSVアプリ」に近づきました。

7日目では、このアプリ全体を
「自分の言葉で説明できるか」
「どこを変えれば何が起きるかイメージできるか」
という視点で、総仕上げしていきます。

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