メインコンテンツへスキップ

🧱 シングルトンは「モジュール」で作る

NIP

🧭 はじめに(What)

このページでは、Python におけるシングルトンの定石を整理する。 結論は明確で、Python ではシングルトンを「クラス」で作らない。 代わりに、モジュールそのものをシングルトンとして使うのが最も自然で、読みやすく、テストしやすい。


🎯 ねらい

    GoF 的な Singleton パターンを Python に持ち込む誤りを正す Python が本来備えている 「唯一性」の表現方法を理解する 過剰設計を避け、意図が一目で伝わる設計を身につける

    🧱 問題の背景

    Python でシングルトンを実装しようとすると、次のような設計に陥りがちだ。

      クラス + __new__ オーバーライド メタクラスを使った Singleton スレッドセーフ性を理由に lock だらけの実装

      これらは一見「ちゃんとしている」ように見えるが、Python では本質的に不要であることが多い。

      Python では import は1回だけ評価され、以降は同じモジュールオブジェクトが再利用される という仕様がある。


      ❌ よくある誤解

      🧬 __new__ をオーバーライドすべき?

        GoF の定義に忠実であろうとする発想 しかし Python では 意図が読み取りづらい 実装を見ないと「シングルトンである」ことが分からない

        🧠 メタクラス Singleton は高度で安全?

          高度だが、読む側の認知負荷が高すぎる チーム開発では「知らないと読めないコード」になる

          🧵 スレッドセーフにしないと危険?

            import 自体は CPython ではスレッドセーフ ほとんどの用途(設定・キャッシュ)では問題にならない

            スレッドセーフ性を理由に 先に複雑化するのは典型的な過剰設計


            💥 悪い例:クラスベース Singleton

            class ConfigSingleton:
                _instance = None
                _lock = Lock()
            
                def __new__(cls):
                    with cls._lock:
                        if cls._instance is None:
                            cls._instance = super().__new__(cls)
                    return cls._instance
            

            問題点

              読まないと意図が分からない テスト時の差し替えが面倒 「唯一性」以外の情報が多すぎる

              この種の実装は GoF の文脈を知らないと理解できない コードになりやすい。


              ✅ 良い例①:モジュールをそのまま使う

              # config.py
              DB_HOST = 'localhost'
              DB_PORT = 5432
              
              # usage.py
              import config
              
              print(config.DB_HOST)
              

              なぜこれで十分か

                import config は一度しか評価されない すでに 「唯一のインスタンス」 が保証されている 読めば一発で意図が分かる

                Python では モジュール = 自然な Singleton


                ✅ 良い例②:遅延生成が必要な場合

                # resource.py
                _resource = None
                
                def get_resource():
                    global _resource
                    if _resource is None:
                        _resource = create_resource()
                    return _resource
                
                  初期化コストが重い場合に有効 依存関係を関数境界に閉じ込められる

                  ✅ 良い例③:lru_cache(maxsize=1) を使う

                  from functools import lru_cache
                  
                  @lru_cache(maxsize=1)
                  def get_config():
                      return load_config()
                  
                    明示的に「1回だけ生成する」ことが分かる テストでは cache_clear() でリセット可能

                    「一度だけ生成される関数」 という意図がコードから直接読める。


                    🧠 設計の要点

                      Python では モジュールが言語組み込みの Singleton 唯一性は import の仕組みが保証している 「クラスである必然性」がない限り、クラスは不要

                      ✅ チェックリスト

                        クラスである必要は本当にあるか? import 1回で目的を満たせないか? テスト時に差し替え・初期化し直せるか? 読んだ人が「これは Singleton だ」と即理解できるか?

                        🔚 まとめ

                        Python においてシングルトンを「設計パターン」として実装しようとすると、 かえって Python らしさを損なう

                          GoF 的 Singleton → 避ける モジュール / 関数 + cache → 推奨

                          シンプルで、読めて、差し替えられる。 それが Python における正しい「唯一性」の設計である。