Python | 自動化:Python アプリ構造化

Python
スポンサーリンク

概要(「1ファイル地獄」から抜け出して、“小さな部品の集まり”にする)

Python アプリ構造化というのは、

「全部 main.py に書きなぐる」のをやめて、
「役割ごとにファイルやフォルダを分けて、部品として組み立てる」

ことです。

自動化スクリプトが育ってくると、

  • 関数が増えすぎて、どこに何があるか分からない
  • ログ、設定ファイル、CLI、処理本体がごちゃ混ぜ
  • ちょっと直すだけで他のところが壊れそうで怖い

みたいな「1ファイル地獄」になりがちです。

構造化のゴールは、「機能を増やしても怖くない状態」にすることです。
そのために、

  • ファイル・フォルダの分け方
  • 役割ごとのコードの置き場所
  • main.py に何を書いて、何を書かないか

を意識して設計していきます。


まずイメージを作る(小さな自動化アプリの全体像)

典型的な「自動化アプリ」の部品

例えば「API からデータを取って、CSV で保存する自動化ツール」を考えます。
それをちゃんと構造化すると、こんな部品に分けられます。

設定を読む部分(config)
コマンドライン引数を処理する部分(cli / argparse)
API を呼ぶ部分(services)
ファイル・ディレクトリを扱う部分(infrastructure)
ログ・エラーを扱う部分(logging)
全体の流れを組み立てる入口(main)

最初は全部1ファイルに書いてもいいですが、
機能が増えたら「役割ごとにモジュールに分ける」方向に育てていきます。

ここから、具体的なディレクトリ構造とコード例を見ながら説明します。


最小構成の「ちゃんとしたアプリ構造」を作る

ステップ1:1ファイルスクリプトの例(構造化前)

まず、よくある「全部入り」パターンから。

# bad_app.py
import requests
import csv
from pathlib import Path

def main():
    input_url = "https://api.example.com/items"
    output_path = Path("data/output.csv")
    output_path.parent.mkdir(exist_ok=True)

    resp = requests.get(input_url, timeout=5)
    resp.raise_for_status()
    items = resp.json()

    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=items[0].keys())
        writer.writeheader()
        writer.writerows(items)

if __name__ == "__main__":
    main()
YAML

最初はこれでいいです。
でも「URLを変えたい」「保存場所を環境ごとに変えたい」「ログを出したい」「CLI から日付を指定したい」と膨らんだ瞬間に、
このファイルがどんどん肥大化します。

これを少しずつ「部品化」していきます。

ステップ2:フォルダを切って、役割ごとにファイルを作る

次のような構造を目指してみます。

my_app/
  main.py          # 入口。全体の流れだけを書く
  config.py        # 設定読み込み(YAML など)
  cli.py           # argparse で引数を処理
  services.py      # ビジネスロジック(API 取得など)
  io_utils.py      # ファイル入出力・ディレクトリ関連
  logging_conf.py  # ログ設定
  config.yaml      # 設定ファイル

この段階で大事なのは、「ファイル名で役割が分かる」ことです。
中身を開かなくても、「ああ、ここにそれが書いてあるんだな」と推測できる状態にします。


各ファイルの役割を例付きで深掘りする

config.py(設定ファイルを読む係)

YAML でも JSON でもいいですが、「設定を読む場所」を1か所にまとめます。

# config.py
import yaml
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
CONFIG_PATH = BASE_DIR / "config.yaml"

def load_config(path: Path = CONFIG_PATH) -> dict:
    with path.open("r", encoding="utf-8") as f:
        return yaml.safe_load(f)
YAML

config.yaml は例えばこう。

api:
  base_url: "https://api.example.com"
  timeout: 5.0

paths:
  output_dir: "data"
YAML

ポイントは、

Python 側は「キー構造」だけ知っていて、値そのものは YAML で変えられる
設定の読み方を変えたいとき(YAML → JSON など)は config.py だけ触ればいい

ということです。

cli.py(コマンドライン引数を扱う係)

argparse を使って、引数処理をまとめます。

# cli.py
import argparse
from datetime import date, timedelta

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="API からデータを取得して保存するツール"
    )
    parser.add_argument(
        "--date",
        help="取得対象日(YYYY-MM-DD)。省略時は昨日。",
        required=False
    )
    return parser

def parse_args():
    parser = build_parser()
    return parser.parse_args()

def resolve_target_date(date_str: str | None) -> date:
    if date_str:
        return date.fromisoformat(date_str)
    return date.today() - timedelta(days=1)
Python

ここで重要なのは、

引数パースのロジックを main.py から追い出している
日付文字列 → date 型への変換も cli 側でやっている

ことです。

main.py は「もう date 型として受け取れる」ので、
中身の処理がシンプルになります。

services.py(ビジネスロジック・API 呼び出し係)

ここには「アプリの目的となる処理」を書きます。
自動化なら「API からデータ取得」「フィルタリング」「集計」などです。

# services.py
import requests

def fetch_items(base_url: str, target_date: str, timeout: float = 5.0) -> list[dict]:
    url = f"{base_url}/items"
    params = {"date": target_date}
    resp = requests.get(url, params=params, timeout=timeout)
    resp.raise_for_status()
    return resp.json()
Python

ここでは、

HTTP という手段は使っているが、
「引数を受け取って結果を返す」関数として設計されている

というのがポイントです。
この関数は CLI を知らないし、設定ファイルも知らないし、ログも知らない。
ただ「仕事」をして結果を返すだけの部品です。

io_utils.py(ファイル保存やディレクトリ操作係)

I/O 周りもまとめておきます。

# io_utils.py
from pathlib import Path
import csv

def ensure_dir(path: Path):
    path.mkdir(parents=True, exist_ok=True)

def save_items_csv(items: list[dict], output_path: Path):
    if not items:
        return
    ensure_dir(output_path.parent)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=items[0].keys())
        writer.writeheader()
        writer.writerows(items)
Python

ここも「CLI や設定を知らない」純粋な I/O 部品です。
Path と items さえ渡せば、仕事をしてくれます。

logging_conf.py(ログの初期化係)

ログも何度も書きたくないので、1か所にまとめます。

# logging_conf.py
import logging
from pathlib import Path

def setup_logging(log_file: Path):
    log_file.parent.mkdir(exist_ok=True)
    logging.basicConfig(
        filename=log_file,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s - %(message)s"
    )
Python

こうしておけば、main.py から

  • ログファイルのパスだけ決める
  • setup_logging を一度呼ぶ

だけで済みます。


main.py には「流れ」だけを書く(ここが一番大事)

最終形の main.py のイメージ

ここまで作った部品を組み立てる入口が main.py です。

# main.py
import logging
from pathlib import Path

from config import load_config, BASE_DIR
from cli import parse_args, resolve_target_date
from services import fetch_items
from io_utils import save_items_csv
from logging_conf import setup_logging

def main():
    config = load_config()
    args = parse_args()
    target_date = resolve_target_date(args.date)

    api_cfg = config["api"]
    paths_cfg = config["paths"]

    output_dir = BASE_DIR / paths_cfg["output_dir"]
    log_file = BASE_DIR / "logs" / "app.log"

    setup_logging(log_file)
    logging.info(f"処理開始 date={target_date}")

    items = fetch_items(
        base_url=api_cfg["base_url"],
        target_date=target_date.isoformat(),
        timeout=api_cfg.get("timeout", 5.0),
    )
    logging.info(f"取得件数: {len(items)}")

    output_path = output_dir / f"items_{target_date}.csv"
    save_items_csv(items, output_path)
    logging.info(f"保存完了: {output_path}")

if __name__ == "__main__":
    main()
Python

ここまで来ると、main.py の役割がすごくはっきりします。

設定を読む(config)
引数を読む(cli)
ログを初期化する(logging_conf)
サービス関数を呼ぶ(services)
I/O 関数を呼ぶ(io_utils)

つまり、「部品と部品を繋ぐだけ」のファイルですが、
それこそが main.py の理想形です。

なぜこれが強いのか(構造化のメリット)

この構造にしておくと、

API の仕様が変わったとき
→ services.py だけ触ればいい

保存形式を CSV から Excel に変えたいとき
→ io_utils.py だけ変えればいい

設定ファイル形式を YAML → JSON に変えたいとき
→ config.py だけ直せばいい

という風に、「変更の影響範囲」が小さくなります。
これが構造化の一番のメリットです。


初心者がハマりやすいポイントと意識してほしいこと

「モジュール間の依存関係」をできるだけ一方向にする

悪い例は、こういう状態です。

main が services を import
services が config を import
config が また services を import

みたいに、「モジュール同士が相互に import し合っている状態」です。
これは循環 import になってエラーの原因にもなるし、何より頭がこんがらがります。

基本の方針としては、

config は誰からも import される“根っこ”
cli も誰からも import される(逆方向には依存しない)
services と io_utils は、config や cli を知らない(引数だけ受け取る)
main だけが「全部を知っている司令塔」

という一方向の依存関係を意識してください。

「とりあえず関数に分ける」から始めて、あとでファイルを分ける

いきなり完璧な構造にしようとする必要はありません。
最初のステップとして、

1ファイルの中で、役割ごとに関数に分ける
→ 後から、関数のグループを別ファイル(モジュール)に移す

という順番で十分です。

例えば、

API 関連の関数が3つほど揃ってきた
→ それをまとめて services.py に移動する

という感じです。

構造化は「最初から完璧にやること」ではなく、
スクリプトが育つのに合わせて「整理し直していくこと」です。


まとめ(構造化は「壊れにくい自動化」を育てる作法)

Python アプリ構造化を、自動化の視点でまとめるとこうなります。

最初は1ファイルで書いてしまっていいが、機能が増えたら「役割ごとにモジュール(ファイル)に分ける」意識を持つ。
設定読み込み(config)、引数処理(cli)、ビジネスロジック(services)、I/O(io_utils)、ログ設定(logging_conf)、入口(main)といった“部品”に分解していく。
main.py は「部品と部品をつなぐだけ」にして、ロジックは関数として別モジュールに追い出していく。
モジュール同士の依存関係を一方向に保つことで、変更の影響範囲を小さくできる。
構造化は一気にやるものではなく、「まず関数に分ける → 役割ごとにファイルを分ける」と段階的に育てていく。

ここまでの考え方がつかめていれば、
今あなたが持っている自動化スクリプトを、
どこからどう分割していくと楽になるか、一緒に具体例まで落とし込めます。

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