设计模式(03)-单例模式的 Python 实现与 AI 基础设施应用

单例模式(Singleton Pattern)的意图只有一句话:确保某个类只有一个实例,并提供全局访问点。它是 GoF 23 种设计模式中最被滥用、也最常被误解的一个。

本文梳理 Python 中几种实现方式的本质差异,以及在 AI 基础设施工程中真实触发这个模式的场景。

四种实现方式

1. Metaclass 拦截

通过自定义元类拦截 __call__

1
2
3
4
5
6
7
8
9
10
11
12
13
class SingletonMeta(type):
_instances: dict = {}

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


class LLMClient(metaclass=SingletonMeta):
def __init__(self, model: str = "claude-opus-4-7"):
self.model = model
self._client = Anthropic()

代价是继承链会变复杂,与其他 Metaclass 组合时容易产生冲突。

2. 基于 __new__

不引入元类,直接在类层面控制:

1
2
3
4
5
6
7
8
class ModelRegistry:
_instance = None

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._registry = {}
return cls._instance

注意 __init__ 在每次构造时仍会执行,需要用 hasattr 守卫初始化逻辑,否则会重复初始化内部状态。

3. 模块级单例(推荐)

Python 模块本身是天然单例——import 机制保证同一模块只被加载一次:

1
2
3
4
5
6
7
8
9
10
11
# clients.py
from anthropic import Anthropic

_client: Anthropic | None = None


def get_client() -> Anthropic:
global _client
if _client is None:
_client = Anthropic()
return _client

这是生产代码里最常见的做法,无需任何模式包装,语义清晰,易于测试时替换。

4. 线程安全版本

多线程环境下存在竞态条件,需要加锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading


class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()

def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None: # double-checked locking
cls._instance = super().__new__(cls)
return cls._instance

外层检查避免每次都获取锁,内层检查防止并发进入临界区后的重复创建。在 asyncio 场景下,把 threading.Lock 换成 asyncio.Lock 并在 async 初始化方法中处理。

在 AI 基础设施中的真实场景

单例模式在 AI infra 层有几个明确的落点:

LLM Client 复用:Anthropic SDK、OpenAI SDK 内部维护 HTTP 连接池,重复创建会浪费资源并影响速率限制计数。Client 应当是进程级单例。

Model Registry:在多 Agent 系统中,各 Agent 需要查询可用模型、路由规则、能力描述。Registry 天然是单例——多个副本意味着状态不一致,路由结果不可预期。

Tool Registry:Harness 层的工具注册中心,Agent 运行时动态查询可用工具,需要进程内唯一:

1
2
3
4
5
6
7
8
9
10
class ToolRegistry(metaclass=SingletonMeta):
def __init__(self):
if not hasattr(self, "_tools"):
self._tools: dict[str, Callable] = {}

def register(self, name: str, fn: Callable) -> None:
self._tools[name] = fn

def get(self, name: str) -> Callable | None:
return self._tools.get(name)

什么时候不该用

单例的核心问题是隐式全局状态——让测试变得困难(需要 mock 或 reset 实例),让依赖关系不透明(调用方看不到它依赖了单例)。

如果在用 FastAPI 或类似 DI 框架,lifespan + dependency injection 是更好的选择;资源的生命周期由框架管理,依赖关系显式声明。

如果在写 Agent 框架,考虑把共享状态通过参数显式传递给每个 Agent,而不是让它们通过全局单例通信——这在 tracing 和调试时会省掉很多麻烦。

单例解决的是资源共享问题,不是架构问题。区分清楚这一点,就知道什么时候该用、什么时候该克制。


设计模式(03)-单例模式的 Python 实现与 AI 基础设施应用
https://www.krli.org/2018/04/03/设计模式-03-单例模式-Python实现/
作者
李科燃
发布于
2018年4月3日
许可协议