はじめに
fastapiを使っているときにサービスクラスをシングルトンにしたいなと思いました。ただ、いろいろやり方はあるらしいのですが、fastapi自体がそれをサポートしているというわけではないみたいです。
ライブラリを入れるのもなぁと思ったので、サクッと継承で解決しようとして失敗したという話です。
問題の概要
シングルトンパターンを継承で実装しようとした時に起こる主な問題:
-
初期化の重複実行: 2回目のインスタンス作成時も
__init__が呼ばれてしまう - 属性の上書き: 既存のインスタンスの状態が意図せず変更される
- 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)