Python で GUI アプリ(PySide / PyQt / Tkinter など)を作っていると、
次のような悩みに一度は直面します。
- class は 1ファイル1つが正解?
- helper class は分けるべき?
-
dialogs.pyが巨大化してきたけど、分けすぎも怖い
私自身、個人開発の時間記録 GUI アプリ(TimeTracker)を作る中で、
「詰め込みすぎ」と「分けすぎ」の両方を経験しました。
この記事では、その実体験をもとに、
- 1ファイルに複数 class を置いてよい条件
- ファイルを分けるべき明確なサイン
- GUI特有の「分けすぎ地獄」を防ぐ考え方
- 分割後に import が壊れない実務的なコツ
を、具体例ベースで整理します。
「設計を綺麗にしたはずなのに、逆に直しづらくなった」
そんな経験がある方の判断材料になれば幸いです。
1. 1ファイルに複数 class を置いてよい場合
次の 3条件すべてを満たすときのみ OK と考えています。
条件①:責務が同じ(1つの目的に収束している)
class NoWheelComboBox(QComboBox):
...
class GapFillDialog(QDialog):
...
class DailyNoteDialog(QDialog):
...
これらはすべて、
- ダイアログ内で完結
- 単発・補助的なUI
- 他画面の思想を持たない
「ダイアログ補助・単発ダイアログ」という同一責務
判断基準
ファイル名を1文で説明できるか?
できなければ責務が混ざっています。
条件②:同時に修正される確率が高い
class DayEditorDialog(QDialog):
...
class _TimeCellEditor(QWidget):
...
_TimeCellEditor は DayEditorDialog 専用
片方を直すと必ずもう片方も直す
条件③:外部から import されない(または1か所のみ)
# OK
from ui.dialogs import DayEditorDialog
# 危険信号
from ui.dialogs import SettingsDialog
from ui.dialogs import ProfileEditorDialog
from ui.dialogs import CategoryEditorDialog
import が増えたら 「分離のサイン」
2. ファイルを分けるべき場合(重要)
以下 どれか1つでも当てはまったら分離 です。
基準①:責務が違う(思想・関心が違う)
TimeTracker の例:
| クラス | 役割 |
|---|---|
DayEditorDialog |
日次実績・計画編集 |
CategoryEditorDialog |
マスタ編集 |
ProfileEditorDialog |
評価ロジック定義 |
SettingsDialog |
システム設定 |
- 思想が完全に別
- 同居させてはいけない
基準②:DB / settings と強く結びついている
UI ←→ DB ←→ settings
特に危険:
SettingsDialogProfileEditorialogCategoryEditorDialog
理由:
- DB構造変更の影響が大きい
- 1ファイルだと 触ったら全部壊れる
基準③:単体で再設計・拡張されうる
例:
- 設定画面をタブ化したい
- 別ウィンドウで開きたい
- 設定UIを全面改修したい
単独ファイルでないと詰む
基準④:ファイルが「スクロール地獄」
目安:
| 状態 | 判断 |
|---|---|
| 〜300行 | 許容 |
| 500行 | 黄色信号 |
| 800行超 | !! 即分離!! |
私の dialogs.py は 2000 行超でした(大反省。。。)
3. TimeTracker に当てはめた正解構成
ui/
├─ dialogs/
│ ├─ day_editor_dialog.py # 今日の一覧・編集
│ ├─ gap_fill_dialog.py # 空白まとめ埋め
│ ├─ category_editor_dialog.py # カテゴリ編集
│ ├─ profile_editor_dialog.py # プロファイル編集
│ ├─ settings_dialog.py # ⚙ 統合設定
│ └─ __init__.py
└─ main_window.py
__init__.py では 外部公開を最小化:
from .day_editor_dialog import DayEditorDialog
from .settings_dialog import SettingsDialog
4. GUI特有の「分けすぎ地獄」とは何か
分離を意識しすぎると、今度はこうなります:
widgets/
editors/
helpers/
controllers/
views/
結果:
- 1つの画面を理解するのに 5ファイル横断
- 修正のたびに import ジャンプ
- 「どこを直せばいいか分からない」
5. 分けすぎ地獄を防ぐ考え方(超重要)
発想を切り替える
GUIでは:「再利用」より「理解しやすさ」を優先する
① 画面単位で“塊”を作る
DayEditorDialog
├─ 内部専用Widget
├─ 内部helper
└─ UIロジック
画面の思考単位 = ファイル単位
② private class は「分けない勇気」
class _TimeCellEditor(QWidget):
...
- 外部に出さない
- 再利用しない
- 同時修正前提
分離しない方が正解なケースは多い
③ GUIでやってはいけない分離
「行数が増えたから」「なんとなく綺麗だから」「MVCっぽくしたいから」
では分けない
GUIは 教科書設計より可読性
6. 迷ったときの実務ルール
- この class を直すとき、隣も直すか?
- import 先は 2か所以上か?
- DB構造変更で一緒に壊れるか?
Yes が2つ以上 → 別ファイル
0〜1個 → 同一ファイルOK
7. 分割後に import が壊れないコツ(GUI分割あるある対策)
ファイル分割をすると、だいたい最初に起きるのがこれです。
- ModuleNotFoundError
- ImportError: cannot import name ...
- 循環 import(circular import)
- 相対 import が混乱
ここでは 「壊れにくい分割の型」 を紹介します。
7-1. ui/dialogs/ を「パッケージ化」する(最重要)
分割したら必ずこの形にします。
ui/
├─ dialogs/
│ ├─ __init__.py
│ ├─ day_editor_dialog.py
│ ├─ settings_dialog.py
│ └─ ...
└─ main_window.py
__init__.py があることで、ui.dialogs を「1つのまとまり」として扱えます。
7-2. 外部公開の import は __init__.py に集約する(バラ撒かない)
外部(main_window.py など)からは 必ずここだけを import する運用にします。
# ui/dialogs/__init__.py
from .day_editor_dialog import DayEditorDialog
from .settings_dialog import SettingsDialog
__all__ = ["DayEditorDialog", "SettingsDialog"]
呼び出し側はこう:
# ui/main_window.py
from ui.dialogs import DayEditorDialog, SettingsDialog
◎メリット
- import 変更が
__init__.pyだけで済む - どこから何を使っていいかが明確
- 将来のファイル名変更にも強い
7-3. 「深い import」を禁止する(壊れやすい)
分割後にやりがちですが、これは NGになりやすい です。
# 壊れやすい(深い import)
from ui.dialogs.day_editor_dialog import DayEditorDialog
7-4. 循環 import を避ける“依存の方向”ルール
GUIで循環 import が起きる典型パターン:
main_window.py が DayEditorDialog を呼ぶ
DayEditorDialog が MainWindow を型ヒント等で参照し始める
双方向依存になり import が死ぬ
ルール:UIの依存は「上→下」へ
MainWindow → Dialog(呼び出す)
Dialog → MainWindow を import しない(参照しない)
Dialog 側はこう考えると壊れません:
必要なものは parent から受け取る
DB などは引数で受け取る
MainWindow の具体クラス名は知らない
理由:
- ファイル名変更・移動に弱い
- 依存が散らばり、管理不能になる
外部は from ui.dialogs import ... で統一 が安全です。
7-5. 型ヒントが原因で循環する場合の回避策
型ヒントを書いた途端に import が循環するの、GUIでよくあります。
◎対策:TYPE_CHECKING を使う
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ui.main_window import MainWindow # 実行時にはimportされない
これで 型チェック時だけ import でき、実行時の循環を回避できます。
7-6. 共通部品は ui/common/ に逃がす(Dialog間の import を減らす)
Dialog 同士で便利関数や widget を共有し始めると、循環の温床になります。
例:色マップ、時間フォーマット、共通ボタン、共通 validator など
そういう時は:
ui/
├─ common/
│ ├─ widgets.py
│ ├─ utils.py
│ └─ constants.py
├─ dialogs/
└─ main_window.py
Dialog → common はOK
Dialog → Dialog は増やさない(増えると壊れ始める)
7-7. 分割時に一番安全な手順(壊れない移行)
分割を一気にやると壊れます。おすすめはこの順番:
ui/dialogs/ を作り __init__.py を置く
まず day_editor_dialog.py 1つだけ移動
import を ui.dialogs 経由に統一
動作確認してから次のDialogへ(反復)
「1個移す→動く」を繰り返すのが最速です。
まとめ
- 基本は 1責務 × 1ファイル
- 例外は「強く結びついたGUI部品」
- GUIは 分けすぎるほど辛くなる
- 目的は「綺麗な設計」ではなく、迷わず直せるコード
補足:本記事の作成プロセスについて
本記事は、筆者が個人開発した Python GUI アプリの設計を振り返りながら、
ChatGPT を用いて以下の点を整理・言語化しました。
- 判断基準の言語化
- 構成の見直し
- 読み手視点での表現調整
最終的な内容・判断軸・結論については、
筆者自身の設計経験と試行錯誤に基づいています。