📁

Pythonプロジェクトのディレクトリ構造設計:個人開発からexe配布まで

に公開

はじめに

個人開発でPythonプロジェクトを進める際、適切なディレクトリ構造は開発効率と保守性を大きく左右します。特に、複数の実行可能プログラムを含み、最終的にexe形式で配布することを前提とした場合、計画的な構造設計が不可欠です。

本記事では、開発からテスト、そしてexe化によるリリースまでをスムーズに行うための実践的なディレクトリ構造を提案します。

推奨ディレクトリ構造

実用的でシンプルな構造

my_project/
├── src/
│   ├── apps/              # 各実行可能プログラム
│   │   ├── __init__.py
│   │   ├── app1_data_analyzer.py
│   │   ├── app2_batch_processor.py
│   │   ├── app3_realtime_monitor.py
│   │   ├── app4_report_generator.py
│   │   ├── app5_data_converter.py
│   │   └── launcher.py
│   ├── lib/               # 共通ライブラリ(シンプルに1階層)
│   │   ├── __init__.py
│   │   ├── processing.py  # メイン処理ロジック
│   │   ├── utils.py       # ユーティリティ関数
│   │   ├── config.py      # 設定管理
│   │   └── fast_calc.pyx  # Cython高速化(必要な場合)
│   └── resources/         # リソースファイル
│       ├── config.json
│       └── templates/
├── tests/                 # テストコード
│   ├── __init__.py
│   ├── test_processing.py
│   └── test_utils.py
├── build/                 # ビルド関連
│   ├── build_exe.py      # exe化スクリプト
│   └── app_specs/        # 各アプリのspecファイル
├── dist/                  # 配布用成果物(自動生成)
├── setup.py              # Cythonビルド用
├── requirements.txt      # 依存パッケージ
├── README.md
└── .gitignore

各ファイルの具体的な実装例

1. src/apps/app1_data_analyzer.py - データ分析アプリ

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
データ分析アプリケーション
数値データから画像を生成し、類似度を計算する
"""
import sys
from pathlib import Path

# srcディレクトリをPythonパスに追加(重要!)
sys.path.insert(0, str(Path(__file__).parent.parent))

from lib import processing, utils, config
import tkinter as tk
from tkinter import filedialog, messagebox
import numpy as np

def main():
    """メインエントリーポイント"""
    # 設定読み込み
    cfg = config.load_config()
    
    # GUIアプリケーション
    root = tk.Tk()
    root.title("データ分析ツール")
    root.geometry("600x400")
    
    def analyze_data():
        # ファイル選択
        file_path = filedialog.askopenfilename(
            filetypes=[("CSVファイル", "*.csv"), ("すべて", "*.*")]
        )
        if not file_path:
            return
        
        try:
            # データ読み込みと処理
            data = utils.load_csv_data(file_path)
            processor = processing.ImageProcessor()
            
            # 画像生成
            image = processor.data_to_image(data, dtype="numerical")
            
            # 結果表示
            messagebox.showinfo("完了", f"画像生成完了\nサイズ: {image.shape}")
            
        except Exception as e:
            messagebox.showerror("エラー", str(e))
    
    # UIセットアップ
    tk.Button(root, text="データ分析開始", command=analyze_data, 
              width=20, height=2).pack(pady=20)
    tk.Button(root, text="終了", command=root.quit, 
              width=20, height=2).pack(pady=10)
    
    root.mainloop()

if __name__ == "__main__":
    main()

2. src/apps/launcher.py - ランチャーアプリケーション

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
プロジェクトランチャー
すべてのアプリケーションを統合管理
"""
import sys
from pathlib import Path
import subprocess
import tkinter as tk
from tkinter import ttk
import json

sys.path.insert(0, str(Path(__file__).parent.parent))

from lib import config

class LauncherApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("プロジェクトランチャー")
        self.root.geometry("400x500")
        
        # アプリケーション定義(同じディレクトリ内のアプリを自動検出)
        self.apps = self._discover_apps()
        self.setup_ui()
    
    def _discover_apps(self):
        """appsディレクトリ内のアプリを自動検出"""
        apps_dir = Path(__file__).parent
        apps = {}
        
        for py_file in apps_dir.glob("app*.py"):
            if py_file.name != "launcher.py":
                # ファイル名から表示名を生成
                display_name = py_file.stem.replace("_", " ").title()
                apps[display_name] = py_file.name
        
        return apps
    
    def setup_ui(self):
        # タイトル
        title_label = ttk.Label(self.root, text="アプリケーション選択", 
                               font=("Arial", 16, "bold"))
        title_label.pack(pady=20)
        
        # アプリケーションボタン
        for name, filename in self.apps.items():
            btn = ttk.Button(
                self.root,
                text=name,
                command=lambda f=filename: self.launch_app(f),
                width=30
            )
            btn.pack(pady=5)
        
        # 終了ボタン
        ttk.Separator(self.root, orient='horizontal').pack(fill='x', pady=20)
        ttk.Button(self.root, text="終了", command=self.root.quit, 
                  width=30).pack(pady=10)
    
    def launch_app(self, filename):
        """アプリケーションを起動"""
        app_path = Path(__file__).parent / filename
        
        # 新しいプロセスで起動
        subprocess.Popen([sys.executable, str(app_path)])
        
        # ログ出力
        print(f"起動: {filename}")
    
    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    app = LauncherApp()
    app.run()

3. src/lib/processing.py - メイン処理ロジック

# -*- coding: utf-8 -*-
"""
画像処理と類似度計算のコアロジック
"""
import cv2
import numpy as np
from typing import Union, List, Dict, Tuple
import warnings

# Cythonモジュールのインポート(オプション)
try:
    from . import fast_calc
    HAS_CYTHON = True
except ImportError:
    HAS_CYTHON = False
    warnings.warn("Cythonモジュールが見つかりません。通常速度で動作します。")

class ImageProcessor:
    """画像処理の主要クラス"""
    
    def __init__(self, use_cython: bool = True):
        self.use_cython = use_cython and HAS_CYTHON
        self.kernels = self._init_kernels()
    
    def _init_kernels(self) -> Dict[str, np.ndarray]:
        """各種カーネルの初期化"""
        return {
            'gaussian': cv2.getGaussianKernel(5, 1.0) @ cv2.getGaussianKernel(5, 1.0).T,
            'sobel_x': np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32),
            'sobel_y': np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32),
            'laplacian': np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32)
        }
    
    def data_to_image(self, data: Union[List, np.ndarray], 
                     dtype: str = "numerical", 
                     size: Tuple[int, int] = (128, 128)) -> np.ndarray:
        """データを画像に変換"""
        if dtype == "numerical":
            return self._numerical_to_image(data, size)
        elif dtype == "categorical":
            return self._categorical_to_image(data, size)
        else:
            raise ValueError(f"サポートされていないデータ型: {dtype}")
    
    def _numerical_to_image(self, data: Union[List[float], np.ndarray], 
                           size: Tuple[int, int]) -> np.ndarray:
        """数値データを画像に変換"""
        # numpy配列に変換
        if not isinstance(data, np.ndarray):
            data = np.array(data, dtype=np.float32)
        
        # データを正規化(0-255)
        data_norm = ((data - data.min()) / (data.max() - data.min()) * 255).astype(np.uint8)
        
        # リサイズして画像化
        if self.use_cython:
            # Cython版の高速リシェイプ
            image = fast_calc.fast_reshape_to_image(data_norm, size[0], size[1])
        else:
            # 通常のリシェイプ
            # データが不足する場合は0パディング
            total_pixels = size[0] * size[1]
            if len(data_norm) < total_pixels:
                data_norm = np.pad(data_norm, (0, total_pixels - len(data_norm)))
            elif len(data_norm) > total_pixels:
                data_norm = data_norm[:total_pixels]
            
            image = data_norm.reshape(size)
        
        return image
    
    def _categorical_to_image(self, data: List[str], 
                             size: Tuple[int, int]) -> np.ndarray:
        """カテゴリデータを画像に変換"""
        # カテゴリを数値に変換
        unique_categories = list(set(data))
        category_map = {cat: idx for idx, cat in enumerate(unique_categories)}
        
        # 数値配列に変換
        numerical_data = [category_map[cat] for cat in data]
        
        # 数値データとして画像化
        return self._numerical_to_image(numerical_data, size)
    
    def calculate_similarity(self, img1: np.ndarray, img2: np.ndarray, 
                           method: str = "mse") -> float:
        """2つの画像の類似度を計算"""
        # サイズを合わせる
        if img1.shape != img2.shape:
            img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
        
        # フィルタ適用
        filtered1 = cv2.filter2D(img1, -1, self.kernels['gaussian'])
        filtered2 = cv2.filter2D(img2, -1, self.kernels['gaussian'])
        
        # 類似度計算
        if method == "mse":
            mse = np.mean((filtered1 - filtered2) ** 2)
            similarity = 1 / (1 + mse)
        elif method == "ssim":
            # 構造的類似性指標
            similarity = self._calculate_ssim(filtered1, filtered2)
        else:
            raise ValueError(f"サポートされていない手法: {method}")
        
        return float(similarity)
    
    def _calculate_ssim(self, img1: np.ndarray, img2: np.ndarray) -> float:
        """SSIM(構造的類似性指標)を計算"""
        # 簡易版SSIM実装
        c1 = 0.01 ** 2
        c2 = 0.03 ** 2
        
        mu1 = cv2.GaussianBlur(img1, (11, 11), 1.5)
        mu2 = cv2.GaussianBlur(img2, (11, 11), 1.5)
        
        mu1_sq = mu1 ** 2
        mu2_sq = mu2 ** 2
        mu1_mu2 = mu1 * mu2
        
        sigma1_sq = cv2.GaussianBlur(img1 ** 2, (11, 11), 1.5) - mu1_sq
        sigma2_sq = cv2.GaussianBlur(img2 ** 2, (11, 11), 1.5) - mu2_sq
        sigma12 = cv2.GaussianBlur(img1 * img2, (11, 11), 1.5) - mu1_mu2
        
        ssim = ((2 * mu1_mu2 + c1) * (2 * sigma12 + c2)) / \
               ((mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2))
        
        return float(np.mean(ssim))

4. src/lib/utils.py - ユーティリティ関数

# -*- coding: utf-8 -*-
"""
ユーティリティ関数群
"""
import csv
import json
from pathlib import Path
from typing import List, Dict, Any
import numpy as np

def load_csv_data(filepath: str) -> List[float]:
    """CSVファイルからデータを読み込み"""
    data = []
    with open(filepath, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        for row in reader:
            # 数値データの列を抽出(最初の列と仮定)
            try:
                value = float(row[0])
                data.append(value)
            except (ValueError, IndexError):
                continue
    
    if not data:
        raise ValueError("有効なデータが見つかりません")
    
    return data

def save_result(result: Dict[str, Any], output_path: str) -> None:
    """結果をJSON形式で保存"""
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(result, f, ensure_ascii=False, indent=2)

def get_timestamp() -> str:
    """タイムスタンプを取得"""
    from datetime import datetime
    return datetime.now().strftime("%Y%m%d_%H%M%S")

def ensure_dir(path: str) -> Path:
    """ディレクトリが存在しない場合は作成"""
    path = Path(path)
    path.mkdir(parents=True, exist_ok=True)
    return path

5. src/lib/config.py - 設定管理

# -*- coding: utf-8 -*-
"""
設定管理モジュール
"""
import json
from pathlib import Path
from typing import Dict, Any

# デフォルト設定
DEFAULT_CONFIG = {
    "image_size": [128, 128],
    "similarity_method": "mse",
    "use_cython": True,
    "output_dir": "output",
    "log_level": "INFO"
}

def get_config_path() -> Path:
    """設定ファイルのパスを取得"""
    # srcの親ディレクトリ(プロジェクトルート)を基準にする
    base_dir = Path(__file__).parent.parent.parent
    return base_dir / "src" / "resources" / "config.json"

def load_config() -> Dict[str, Any]:
    """設定を読み込み"""
    config_path = get_config_path()
    
    if config_path.exists():
        with open(config_path, 'r', encoding='utf-8') as f:
            user_config = json.load(f)
        # デフォルト設定とマージ
        config = DEFAULT_CONFIG.copy()
        config.update(user_config)
        return config
    else:
        # 設定ファイルがない場合はデフォルトを使用
        save_config(DEFAULT_CONFIG)
        return DEFAULT_CONFIG

def save_config(config: Dict[str, Any]) -> None:
    """設定を保存"""
    config_path = get_config_path()
    config_path.parent.mkdir(parents=True, exist_ok=True)
    
    with open(config_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, ensure_ascii=False, indent=2)

def get(key: str, default: Any = None) -> Any:
    """設定値を取得"""
    config = load_config()
    return config.get(key, default)

6. src/lib/fast_calc.pyx - Cython高速化モジュール(オプション)

# -*- coding: utf-8 -*-
# cython: language_level=3
"""
Cythonによる高速化処理
コンパイルが必要: python setup.py build_ext --inplace
"""
import numpy as np
cimport numpy as np
cimport cython

# NumPy C-APIの初期化
np.import_array()

@cython.boundscheck(False)
@cython.wraparound(False)
def fast_reshape_to_image(np.ndarray[np.uint8_t, ndim=1] data, 
                         int width, int height):
    """高速な1次元配列から2次元画像への変換"""
    cdef int i, j, idx
    cdef int data_len = data.shape[0]
    cdef np.ndarray[np.uint8_t, ndim=2] result = np.zeros((height, width), dtype=np.uint8)
    
    for i in range(height):
        for j in range(width):
            idx = i * width + j
            if idx < data_len:
                result[i, j] = data[idx]
            else:
                result[i, j] = 0  # パディング
    
    return result

@cython.boundscheck(False)
@cython.wraparound(False)
def fast_convolution_2d(np.ndarray[np.float32_t, ndim=2] image,
                       np.ndarray[np.float32_t, ndim=2] kernel):
    """高速2D畳み込み演算"""
    cdef int img_h = image.shape[0]
    cdef int img_w = image.shape[1]
    cdef int ker_h = kernel.shape[0]
    cdef int ker_w = kernel.shape[1]
    cdef int pad_h = ker_h // 2
    cdef int pad_w = ker_w // 2
    
    # 結果配列
    cdef np.ndarray[np.float32_t, ndim=2] result = np.zeros_like(image)
    
    cdef int i, j, m, n
    cdef float sum_val
    
    for i in range(pad_h, img_h - pad_h):
        for j in range(pad_w, img_w - pad_w):
            sum_val = 0.0
            for m in range(ker_h):
                for n in range(ker_w):
                    sum_val += image[i + m - pad_h, j + n - pad_w] * kernel[m, n]
            result[i, j] = sum_val
    
    return result

ビルド設定

setup.py - Cythonビルド設定

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Cythonモジュールのビルド設定
使用方法: python setup.py build_ext --inplace
"""
from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy as np
import sys
from pathlib import Path

# プロジェクトのルートディレクトリ
ROOT_DIR = Path(__file__).parent

# Cython拡張モジュールの定義
extensions = [
    Extension(
        name="src.lib.fast_calc",  # モジュールの完全な名前
        sources=["src/lib/fast_calc.pyx"],  # Cythonソースファイル
        include_dirs=[np.get_include()],  # NumPyのヘッダファイルパス
        language="c",  # C言語を使用(C++の場合は"c++")
    )
]

# セットアップ設定
setup(
    name="MyProjectCython",
    ext_modules=cythonize(
        extensions,
        compiler_directives={
            'language_level': "3",  # Python 3を使用
            'boundscheck': False,   # 境界チェックを無効化(高速化)
            'wraparound': False,    # 負のインデックスを無効化(高速化)
        }
    ),
    zip_safe=False,
)

# ビルド後の説明を表示
if "build_ext" in sys.argv:
    print("\n" + "="*50)
    print("Cythonモジュールのビルドが完了しました!")
    print("生成されたファイル:")
    print("  - src/lib/fast_calc.*.pyd (Windows)")
    print("  - src/lib/fast_calc.*.so (Linux/Mac)")
    print("="*50 + "\n")

requirements.txt - 依存パッケージ

# 基本パッケージ
numpy>=1.20.0
opencv-python>=4.5.0

# GUI
tkinter  # 通常はPythonに含まれている

# ビルド用(オプション)
Cython>=0.29.0  # Cython高速化を使用する場合
pyinstaller>=5.0  # exe化用

# テスト用(開発時のみ)
pytest>=7.0.0
pytest-cov>=3.0.0

build/build_exe.py - exe化スクリプト

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
exe化ビルドスクリプト
全アプリケーションを一括でexe化する
"""
import os
import sys
import shutil
import subprocess
from pathlib import Path
import json

# プロジェクトルート
ROOT_DIR = Path(__file__).parent.parent
SRC_DIR = ROOT_DIR / "src"
DIST_DIR = ROOT_DIR / "dist"
BUILD_DIR = ROOT_DIR / "build"

class ExeBuilder:
    def __init__(self):
        self.apps_dir = SRC_DIR / "apps"
        self.specs_dir = BUILD_DIR / "app_specs"
        self.specs_dir.mkdir(exist_ok=True)
    
    def create_spec_file(self, app_name: str, app_path: Path) -> Path:
        """各アプリ用のspecファイルを生成"""
        spec_content = f'''
# -*- mode: python ; coding: utf-8 -*-
# {app_name}のビルド設定

block_cipher = None

a = Analysis(
    ['{app_path.as_posix()}'],
    pathex=['{SRC_DIR.as_posix()}'],
    binaries=[],
    datas=[
        ('{(SRC_DIR / "resources").as_posix()}', 'resources'),
        ('{(SRC_DIR / "lib").as_posix()}', 'lib'),
    ],
    hiddenimports=['lib', 'lib.processing', 'lib.utils', 'lib.config'],
    hookspath=[],
    hooksconfig={{}},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='{app_name}',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,  # GUIアプリの場合False、CLIアプリの場合True
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
'''
        
        spec_path = self.specs_dir / f"{app_name}.spec"
        spec_path.write_text(spec_content, encoding='utf-8')
        return spec_path
    
    def build_app(self, app_file: Path) -> bool:
        """個別アプリをビルド"""
        app_name = app_file.stem
        print(f"\n{'='*50}")
        print(f"ビルド開始: {app_name}")
        print(f"{'='*50}")
        
        # specファイル作成
        spec_path = self.create_spec_file(app_name, app_file)
        
        # PyInstallerでビルド
        cmd = [
            sys.executable, "-m", "PyInstaller",
            "--clean",
            "--noconfirm",
            "--distpath", str(DIST_DIR),
            "--workpath", str(BUILD_DIR / "work"),
            str(spec_path)
        ]
        
        try:
            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
            print(f"✓ {app_name} のビルドが成功しました")
            return True
        except subprocess.CalledProcessError as e:
            print(f"✗ {app_name} のビルドが失敗しました")
            print(f"エラー: {e.stderr}")
            return False
    
    def build_all(self):
        """全アプリケーションをビルド"""
        print("全アプリケーションのビルドを開始します...")
        
        # distディレクトリをクリーン
        if DIST_DIR.exists():
            shutil.rmtree(DIST_DIR)
        DIST_DIR.mkdir(exist_ok=True)
        
        # アプリケーションファイルを検索
        app_files = list(self.apps_dir.glob("app*.py"))
        app_files.append(self.apps_dir / "launcher.py")
        
        success_count = 0
        for app_file in app_files:
            if self.build_app(app_file):
                success_count += 1
        
        print(f"\n{'='*50}")
        print(f"ビルド完了: {success_count}/{len(app_files)} 成功")
        print(f"成果物: {DIST_DIR}")
        print(f"{'='*50}")
        
        # 配布用ZIPを作成
        if success_count == len(app_files):
            self.create_release_zip()
    
    def create_release_zip(self):
        """配布用ZIPファイルを作成"""
        from datetime import datetime
        
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        zip_name = f"MyProject_Release_{timestamp}"
        
        print(f"\n配布用ZIPを作成中: {zip_name}.zip")
        shutil.make_archive(
            str(DIST_DIR / zip_name),
            'zip',
            DIST_DIR
        )
        print(f"✓ 配布用ZIPを作成しました: {zip_name}.zip")

if __name__ == "__main__":
    builder = ExeBuilder()
    builder.build_all()

.gitignore - Git除外設定

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.pyd
.Python
venv/
env/

# Cython
*.c
*.cpp
build/
*.egg-info/

# PyInstaller
dist/
build/
*.spec

# IDE
.vscode/
.idea/
*.swp
*.swo

# プロジェクト固有
output/
logs/
*.log
.DS_Store
Thumbs.db

開発ワークフロー

1. 環境セットアップ

# 仮想環境の作成
python -m venv venv

# 仮想環境の有効化 (Windows)
venv\Scripts\activate

# 仮想環境の有効化 (Mac/Linux)
source venv/bin/activate

# 依存関係のインストール
pip install -r requirements.txt

# Cythonモジュールのビルド(オプション)
python setup.py build_ext --inplace

2. 開発時の実行

# 個別アプリの実行
python src/apps/app1_data_analyzer.py

# ランチャーから実行
python src/apps/launcher.py

# テストの実行
pytest tests/

# カバレッジ付きテスト
pytest tests/ --cov=src.lib --cov-report=html

3. exe化と配布

# 全アプリケーションのexe化
python build/build_exe.py

# 特定のアプリだけexe化する場合
pyinstaller --onefile --windowed src/apps/launcher.py

4. プロジェクト初期化コマンド

新しいプロジェクトを始める際のコマンド集:

# プロジェクトディレクトリ構造の作成
mkdir -p src/{apps,lib,resources}
mkdir -p tests build/app_specs
touch src/__init__.py src/apps/__init__.py src/lib/__init__.py
touch README.md requirements.txt setup.py .gitignore

# Gitリポジトリの初期化
git init
git add .
git commit -m "Initial project structure"

ベストプラクティス

1. パス管理の統一

開発環境とexe環境の両方で動作するパス解決:

# src/lib/utils.pyに追加
import sys
from pathlib import Path

def get_project_root() -> Path:
    """プロジェクトルートディレクトリを取得"""
    if getattr(sys, 'frozen', False):
        # exe化されている場合
        return Path(sys.executable).parent
    else:
        # 開発環境の場合(srcの親ディレクトリ)
        return Path(__file__).parent.parent.parent

def get_resource_path(relative_path: str) -> Path:
    """リソースファイルの絶対パスを取得"""
    if hasattr(sys, '_MEIPASS'):
        # PyInstallerの一時ディレクトリ
        return Path(sys._MEIPASS) / relative_path
    else:
        return get_project_root() / "src" / relative_path

2. エラーハンドリング

適切なエラー処理でユーザーフレンドリーに:

# src/apps/app1_data_analyzer.pyの改良版
import sys
import traceback
from tkinter import messagebox

def main():
    try:
        # アプリケーションのメイン処理
        app = DataAnalyzerApp()
        app.run()
    except Exception as e:
        # エラー詳細をログに記録
        error_msg = f"エラーが発生しました:\n{str(e)}\n\n詳細:\n{traceback.format_exc()}"
        
        # ログファイルに保存
        log_path = Path(sys.executable).parent / "error.log" if getattr(sys, 'frozen', False) else "error.log"
        with open(log_path, 'a', encoding='utf-8') as f:
            f.write(f"\n{'='*50}\n{datetime.now()}\n{error_msg}\n")
        
        # ユーザーに通知
        messagebox.showerror("エラー", f"エラーが発生しました。\n詳細は {log_path} を確認してください。")
        sys.exit(1)

3. メモリ管理

大量データ処理時のメモリ最適化:

# src/lib/processing.pyに追加
import gc

class ImageProcessor:
    def process_large_dataset(self, data_list: List[np.ndarray]) -> List[float]:
        """大量データのバッチ処理"""
        results = []
        batch_size = 100  # メモリに応じて調整
        
        for i in range(0, len(data_list), batch_size):
            batch = data_list[i:i + batch_size]
            
            # バッチ処理
            for data in batch:
                result = self.calculate_similarity(data, self.reference_image)
                results.append(result)
            
            # メモリ解放
            del batch
            gc.collect()
        
        return results

4. テストの書き方

# tests/test_processing.py
import pytest
import numpy as np
from src.lib.processing import ImageProcessor

class TestImageProcessor:
    @pytest.fixture
    def processor(self):
        return ImageProcessor(use_cython=False)  # テストではCython無効
    
    def test_data_to_image_numerical(self, processor):
        data = [1.0, 2.0, 3.0, 4.0]
        image = processor.data_to_image(data, "numerical", (2, 2))
        
        assert image.shape == (2, 2)
        assert image.dtype == np.uint8
    
    def test_calculate_similarity_identical(self, processor):
        img = np.ones((10, 10), dtype=np.uint8) * 128
        similarity = processor.calculate_similarity(img, img)
        
        assert similarity == 1.0  # 同じ画像なので完全一致

トラブルシューティング

よくある問題と解決方法

1. ModuleNotFoundError: No module named 'lib'

# 原因:Pythonパスが設定されていない
# 解決方法:各アプリの先頭に以下を追加
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))

2. exe化後にリソースファイルが見つからない

# specファイルのdatasにリソースを追加
datas=[
    ('src/resources', 'resources'),
    ('src/lib', 'lib'),  # libフォルダも含める
]

3. OpenCVのImportError

# opencv-python-headlessを使用(GUI不要の場合)
pip uninstall opencv-python
pip install opencv-python-headless

4. Cythonモジュールがインポートできない

# オプショナルにしてフォールバックを実装
try:
    from . import fast_calc
    HAS_CYTHON = True
except ImportError:
    HAS_CYTHON = False
    # 通常のPython実装を使用

まとめ

本記事で提案したシンプルなディレクトリ構造により、以下のメリットが得られます:

  1. シンプルさ:フラットな構造でファイルを見つけやすい
  2. 実用性:5-6個のアプリに最適な規模
  3. 拡張性:必要に応じて構造を複雑化できる
  4. 保守性:共通処理をlibフォルダにまとめて管理
  5. exe化対応:ビルドスクリプトで簡単に配布可能

個人開発では「シンプルに始めて、必要に応じて成長させる」アプローチが最も効果的です。

この構造をベースに、あなたのプロジェクトに合わせてカスタマイズしてください。

GitHubで編集を提案

Discussion