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

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

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

四种实现方式

1. Metaclass 拦截

通过自定义元类拦截 __call__:

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__

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

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 机制保证同一模块只被加载一次:

# 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. 线程安全版本

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

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 运行时动态查询可用工具,需要进程内唯一:

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 和调试时会省掉很多麻烦。

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

设计模式AI 基础设施Python设计模式创建型模式单例模式
← 所有文章