0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python GUIで「1ファイルに複数class」はどこまで許されるか ── 分けすぎ地獄を避ける設計判断(実例付き)

Last updated at Posted at 2026-01-15

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):
    ...

_TimeCellEditorDayEditorDialog 専用
片方を直すと必ずもう片方も直す

条件③:外部から 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

特に危険:

  • SettingsDialog
  • ProfileEditorialog
  • CategoryEditorDialog

理由:

  • 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.pyDayEditorDialog を呼ぶ
DayEditorDialogMainWindow を型ヒント等で参照し始める

双方向依存になり import が死ぬ

ルール:UIの依存は「上→下」へ

MainWindowDialog(呼び出す)
DialogMainWindow を 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

Dialogcommon はOK
DialogDialog は増やさない(増えると壊れ始める)

7-7. 分割時に一番安全な手順(壊れない移行)

分割を一気にやると壊れます。おすすめはこの順番:

ui/dialogs/ を作り __init__.py を置く
まず day_editor_dialog.py 1つだけ移動
import を ui.dialogs 経由に統一
動作確認してから次のDialogへ(反復)

「1個移す→動く」を繰り返すのが最速です。

まとめ

  • 基本は 1責務 × 1ファイル
  • 例外は「強く結びついたGUI部品」
  • GUIは 分けすぎるほど辛くなる
  • 目的は「綺麗な設計」ではなく、迷わず直せるコード

補足:本記事の作成プロセスについて

本記事は、筆者が個人開発した Python GUI アプリの設計を振り返りながら、
ChatGPT を用いて以下の点を整理・言語化しました。

  • 判断基準の言語化
  • 構成の見直し
  • 読み手視点での表現調整

最終的な内容・判断軸・結論については、
筆者自身の設計経験と試行錯誤に基づいています。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?