0
0

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で継承方式のシングルトンパターンが失敗する理由とメタクラス方式による解決法

Posted at

はじめに

fastapiを使っているときにサービスクラスをシングルトンにしたいなと思いました。ただ、いろいろやり方はあるらしいのですが、fastapi自体がそれをサポートしているというわけではないみたいです。

ライブラリを入れるのもなぁと思ったので、サクッと継承で解決しようとして失敗したという話です。

問題の概要

シングルトンパターンを継承で実装しようとした時に起こる主な問題:

  1. 初期化の重複実行: 2回目のインスタンス作成時も__init__が呼ばれてしまう
  2. 属性の上書き: 既存のインスタンスの状態が意図せず変更される
  3. ABC(抽象基底クラス)との組み合わせの複雑さ

実験環境

# 実行方法
uv run main.py

継承方式の実装と問題点

継承方式のシングルトン実装

class SingletonBase:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__new__(cls)
        return cls._instances[cls]

    def __init__(self, name="default"):
        self.name = name
        self.created_at = time.time()

実際の問題

# 1回目の作成
user1 = UserService("service1")
print(f"user1.name: {user1.name}")  # → "service1"

# 2回目の作成(同じインスタンスが返される)
user2 = UserService("service2")
print(f"user2.name: {user2.name}")  # → "service2" ⚠️ 
print(f"user1.name: {user1.name}")  # → "service2" ⚠️ 上書きされた!
print(f"同一インスタンス: {user1 is user2}")  # → True

問題点: __new__で同じインスタンスを返しても、__init__は毎回呼ばれるため、既存インスタンスの状態が上書きされてしまいます。

メタクラス方式による解決

ABCMetaを継承したメタクラス実装

from abc import ABCMeta

class SingletonABCMeta(ABCMeta):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

メタクラス方式の利点

# 1回目の作成
meta_user1 = MetaUserService("meta_service1")
print(f"meta_user1.name: {meta_user1.name}")  # → "meta_service1"

# 2回目の作成
meta_user2 = MetaUserService("meta_service2")
print(f"meta_user2.name: {meta_user2.name}")  # → "meta_service1" ✅ 
print(f"meta_user1.name: {meta_user1.name}")  # → "meta_service1" ✅ 変わらない!
print(f"同一インスタンス: {meta_user1 is meta_user2}")  # → True

利点: メタクラスの__call__メソッドで制御することで、既存インスタンスがある場合は__init__の実行をスキップできます。

実行結果の比較

継承方式の実行ログ

👤 1回目:UserService('service1')を作成
🔍 UserServiceの__new__メソッドが呼ばれました
  ✨ UserServiceの新しいインスタンスを作成中...
  🚀 UserServiceの__init__メソッドが呼ばれました (name=service1)

👤 2回目:UserService('service2')を作成
🔍 UserServiceの__new__メソッドが呼ばれました
  ♻️  UserServiceの既存インスタンスを返します
  🚀 UserServiceの__init__メソッドが呼ばれました (name=service2) ⚠️

メタクラス方式の実行ログ

👤 1回目:MetaUserService('meta_service1')を作成
🔍 MetaUserServiceのメタクラス__call__が呼ばれました
  ✨ MetaUserServiceの新しいインスタンスを作成中...

👤 2回目:MetaUserService('meta_service2')を作成
🔍 MetaUserServiceのメタクラス__call__が呼ばれました
  ♻️  MetaUserServiceの既存インスタンスを返します ✅

FastAPIでの検証

実際のWebアプリケーションでの動作も確認できます:

@app.get("/compare")
def compare_methods():
    # 両方式の動作を比較するエンドポイント
    # 実際のレスポンスでシングルトンの動作を確認可能
# サーバー起動
uv run main.py

# エンドポイントテスト
curl http://localhost:8000/compare

まとめ

Pythonでシングルトンパターンを実装する場合は、メタクラス方式を使用することで、予期しない初期化の重複を避けることができます。

参考情報

ソース全体

main.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "fastapi",
#     "uvicorn",
# ]
# ///

# FastAPI シングルトン継承問題の再現(修正版)

from fastapi import FastAPI
from abc import ABC, abstractmethod
import time

app = FastAPI()

print("=== 継承方式のシングルトンパターン ===")


# 1. 継承方式でシングルトンを実装
class SingletonBase:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        print(f"🔍 {cls.__name__}の__new__メソッドが呼ばれました")
        if cls not in cls._instances:
            print(f"{cls.__name__}の新しいインスタンスを作成中...")
            cls._instances[cls] = super().__new__(cls)
        else:
            print(f"  ♻️  {cls.__name__}の既存インスタンスを返します")
        return cls._instances[cls]

    def __init__(self, name="default"):
        print(
            f"  🚀 {self.__class__.__name__}の__init__メソッドが呼ばれました (name={name})"
        )
        self.name = name
        self.created_at = time.time()
        print(f"{self.__class__.__name__}の初期化完了")


# 2. ABC機能も継承で追加
class ServiceBase(SingletonBase, ABC):
    @abstractmethod
    def get_data(self):
        pass


# 3. インターフェース
class UserServiceInterface(ServiceBase):
    @abstractmethod
    def get_user(self, user_id: str):
        pass


# 4. 実装クラス
class UserService(UserServiceInterface):
    def __init__(self, name="default"):
        print(f"    📝 UserService.__init__開始 (name={name})")
        super().__init__(name)
        print(f"    📝 UserService.__init__終了")

    def get_data(self):
        return f"UserService data from {self.name}"

    def get_user(self, user_id: str):
        return f"User {user_id} from {self.name}"


class MockUserService(UserServiceInterface):
    def __init__(self, name="default"):
        print(f"    🎭 MockUserService.__init__開始 (name={name})")
        super().__init__(name)
        print(f"    🎭 MockUserService.__init__終了")

    def get_data(self):
        return f"MOCK data from {self.name}"

    def get_user(self, user_id: str):
        return f"MOCK User {user_id} from {self.name}"


# テスト実行
print("\n--- UserServiceのテスト ---")
print("👤 1回目:UserService('service1')を作成")
user1 = UserService("service1")
print(f"   結果:id={id(user1)}, name={user1.name}")

print("\n👤 2回目:UserService('service2')を作成")
user2 = UserService("service2")  # nameは'service1'のまま(既存インスタンス)
print(f"   結果:id={id(user2)}, name={user2.name}")
print(f"   🔗 同一インスタンス? {user1 is user2}")

print("\n--- MockUserServiceのテスト ---")
print("🎭 1回目:MockUserService('mock1')を作成")
mock1 = MockUserService("mock1")
print(f"   結果:id={id(mock1)}, name={mock1.name}")

print("\n🎭 2回目:MockUserService('mock2')を作成")
mock2 = MockUserService("mock2")
print(f"   結果:id={id(mock2)}, name={mock2.name}")
print(f"   🔗 同一インスタンス? {mock1 is mock2}")

print(f"\n❓ UserService vs MockUserService 同一? {user1 is mock1}")
print(f"📊 インスタンス辞書:{SingletonBase._instances}")

print("\n" + "=" * 60)
print("=== メタクラス方式のシングルトンパターン ===")

# メタクラス方式 - ABCMetaを継承してメタクラス衝突を解決
from abc import ABCMeta


class SingletonABCMeta(ABCMeta):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        print(f"🔍 {cls.__name__}のメタクラス__call__が呼ばれました")
        if cls not in cls._instances:
            print(f"{cls.__name__}の新しいインスタンスを作成中...")
            cls._instances[cls] = super().__call__(*args, **kwargs)
        else:
            print(f"  ♻️  {cls.__name__}の既存インスタンスを返します")
        return cls._instances[cls]


class MetaServiceBase(ABC, metaclass=SingletonABCMeta):
    def __init__(self, name="default"):
        print(f"  🚀 {self.__class__.__name__}の初期化開始 (name={name})")
        self.name = name
        self.created_at = time.time()
        print(f"{self.__class__.__name__}の初期化完了")

    @abstractmethod
    def get_data(self):
        pass


class MetaUserServiceInterface(MetaServiceBase):
    @abstractmethod
    def get_user(self, user_id: str):
        pass


class MetaUserService(MetaUserServiceInterface):
    def __init__(self, name="default"):
        print(f"    📝 MetaUserService.__init__開始 (name={name})")
        super().__init__(name)
        print(f"    📝 MetaUserService.__init__終了")

    def get_data(self):
        return f"MetaUserService data from {self.name}"

    def get_user(self, user_id: str):
        return f"Meta User {user_id} from {self.name}"


class MetaMockUserService(MetaUserServiceInterface):
    def __init__(self, name="default"):
        print(f"    🎭 MetaMockUserService.__init__開始 (name={name})")
        super().__init__(name)
        print(f"    🎭 MetaMockUserService.__init__終了")

    def get_data(self):
        return f"META MOCK data from {self.name}"

    def get_user(self, user_id: str):
        return f"META MOCK User {user_id} from {self.name}"


# テスト実行
print("\n--- MetaUserServiceのテスト ---")
print("👤 1回目:MetaUserService('meta_service1')を作成")
meta_user1 = MetaUserService("meta_service1")
print(f"   結果:id={id(meta_user1)}, name={meta_user1.name}")

print("\n👤 2回目:MetaUserService('meta_service2')を作成")
meta_user2 = MetaUserService("meta_service2")
print(f"   結果:id={id(meta_user2)}, name={meta_user2.name}")
print(f"   🔗 同一インスタンス? {meta_user1 is meta_user2}")

print("\n--- MetaMockUserServiceのテスト ---")
print("🎭 1回目:MetaMockUserService('meta_mock1')を作成")
meta_mock1 = MetaMockUserService("meta_mock1")
print(f"   結果:id={id(meta_mock1)}, name={meta_mock1.name}")

print("\n🎭 2回目:MetaMockUserService('meta_mock2')を作成")
meta_mock2 = MetaMockUserService("meta_mock2")
print(f"   結果:id={id(meta_mock2)}, name={meta_mock2.name}")
print(f"   🔗 同一インスタンス? {meta_mock1 is meta_mock2}")

print(f"\n❓ MetaUserService vs MetaMockUserService 同一? {meta_user1 is meta_mock1}")
print(f"📊 メタクラスインスタンス辞書:{SingletonABCMeta._instances}")


# FastAPI エンドポイント
@app.get("/inheritance-test")
def test_inheritance():
    """継承方式のシングルトンテスト"""
    print("\n🌐 API呼び出し:継承方式テスト開始")
    user1 = UserService("api_call_1")
    user2 = UserService("api_call_2")

    return {
        "方式": "継承方式",
        "user1_id": id(user1),
        "user2_id": id(user2),
        "同一インスタンス": user1 is user2,
        "user1_name": user1.name,
        "user2_name": user2.name,
        "user1_data": user1.get_data(),
        "インスタンス数": len(SingletonBase._instances),
        "説明": "継承方式では2回目の初期化でも__init__が呼ばれ、属性が上書きされる",
    }


@app.get("/metaclass-test")
def test_metaclass():
    """メタクラス方式のシングルトンテスト"""
    print("\n🌐 API呼び出し:メタクラス方式テスト開始")
    meta_user1 = MetaUserService("meta_api_1")
    meta_user2 = MetaUserService("meta_api_2")

    return {
        "方式": "メタクラス方式",
        "meta_user1_id": id(meta_user1),
        "meta_user2_id": id(meta_user2),
        "同一インスタンス": meta_user1 is meta_user2,
        "meta_user1_name": meta_user1.name,
        "meta_user2_name": meta_user2.name,
        "meta_user1_data": meta_user1.get_data(),
        "インスタンス数": len(SingletonABCMeta._instances),
        "説明": "メタクラス方式では既存インスタンスがある場合__init__が呼ばれない",
    }


@app.get("/compare")
def compare_methods():
    """両方式の比較"""
    # 継承方式
    inherit_user1 = UserService("inherit_1")
    inherit_user2 = UserService("inherit_2")

    # メタクラス方式
    meta_user1 = MetaUserService("meta_1")
    meta_user2 = MetaUserService("meta_2")

    return {
        "継承方式": {
            "同一インスタンス": inherit_user1 is inherit_user2,
            "1回目のname": inherit_user1.name,
            "2回目のname": inherit_user2.name,
            "問題": "2回目の初期化で属性が上書きされる可能性がある",
        },
        "メタクラス方式": {
            "同一インスタンス": meta_user1 is meta_user2,
            "1回目のname": meta_user1.name,
            "2回目のname": meta_user2.name,
            "利点": "既存インスタンスがある場合、初期化をスキップ",
        },
        "推奨": "メタクラス方式がより安全で予測可能",
    }


if __name__ == "__main__":
    print("\n🚀 FastAPIサーバーを起動しています...")
    print("📱 テスト用エンドポイント:")
    print("   GET /inheritance-test  - 継承方式のテスト")
    print("   GET /metaclass-test    - メタクラス方式のテスト")
    print("   GET /compare          - 両方式の比較")

    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?