在 Python 工程里,经常会遇到这样一种需求:某个类的对象在整个程序运行期间只能有一个,比如全局配置管理器、数据库连接池或者日志处理器。如果到处都去 new 一个出来,不仅浪费资源,还容易出现状态不一致的问题。要让一个类实例在项目中被全局共享,本质上就是实现单例并提供一个统一的访问入口。

为什么需要全局共享单个类实例
当项目规模变大以后,很多模块都要读取同样的配置或者写入同一个日志文件。假设每个模块都自己创建一个配置对象,一旦某个地方修改了配置,其他模块完全感知不到,这就造成了数据割裂。把实例做成全局唯一,所有模块拿到的都是同一个对象,任何修改都能即时生效。
另外,有些资源本身就不适合重复创建,例如数据库连接、线程池、Redis 客户端。频繁实例化不仅会拖慢启动速度,还可能把连接数打满。通过全局共享单个类实例,可以把这些昂贵资源的创建控制在一次以内,后面谁用谁拿,既省心又安全。
利用模块特性实现最简单共享
Python 的模块在第一次被 import 时执行一遍,之后解释器会缓存到 sys.modules 里,后续再 import 拿到的是同一个模块对象。因此,把一个类的实例直接写在模块顶层,这个实例天然就是进程内单例。
下面这段代码把配置类实例化后赋值给模块变量,别的文件只要 from config import settings 就能拿到同一个对象:
# config.py
class Settings:
def __init__(self):
self.debug = True
self.language = 'zh_CN'
# 模块加载时创建一次,全局共享
settings = Settings()
# user.py
from config import settings
def print_lang():
print(settings.language)
这种写法几乎没有额外代码,也最容易理解。缺点是类本身还能被别人手动再实例化,如果你希望从语法层面禁止重复创建,就需要更严格的约束。
使用 __new__ 方法控制实例化
在类里面重写 __new__,可以判断类属性里是否已经有实例,有就直接返回,没有才真正创建。这样无论谁调用类名,拿到的都是同一个对象。
示例中使用一个下划线开头的变量保存实例,并在 __new__ 里加锁,避免多线程同时进来创建出两个对象:
import threading
class Singleton:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value=None):
# 防止重复初始化覆盖属性
if not hasattr(self, 'value'):
self.value = value
a = Singleton(10)
b = Singleton(20)
print(a is b) # True
print(a.value) # 10,不会被 20 覆盖
这里要注意 __init__ 每次调用类都会执行,所以用了 hasattr 做保护,否则第二个调用者可能把第一个设的值改掉。双检锁写法在多线程下是安全的,但代码稍显啰嗦。
用元类统一约束所有单例类
如果项目里有好几个类都要做成单例,每次都写一遍 __new__ 太重复。可以定义一个元类,在创建类的时候注入单例逻辑,以后需要单例的类只要指定 metaclass 即可。
元类通过重写 __call__ 来控制类被调用时的行为,下面的例子演示了如何用一个元类让任意类变成全局唯一:
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self):
self.lines = []
def log(self, msg):
self.lines.append(msg)
log1 = Logger()
log2 = Logger()
print(log1 is log2) # True
元类方案把单例逻辑集中起来,业务类写起来非常干净。不过元类对初学者不太友好,调试时调用栈会多一层,需要团队能接受这种写法。
装饰器方式实现全局共享
如果不想碰元类,也可以用装饰器把类包一层,在装饰器内部维护实例字典。被装饰的类自动变成单例,代码直观且复用方便。
下面这个装饰器支持不同类各自保存自己的实例,也加了线程锁:
def singleton(cls):
instances = {}
lock = threading.Lock()
def get_instance(*args, **kwargs):
if cls not in instances:
with lock:
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Cache:
def __init__(self):
self.store = {}
c1 = Cache()
c2 = Cache()
print(c1 is c2) # True
装饰器比元类轻量,阅读成本更低。但它会把原类的身份变成函数,有些依赖 isinstance 的类型检查要写成 isinstance(obj, Cache) 时得注意 Cache 已被替换,可以用 functools.wraps 或保留原类引用解决。
不同方案的选择建议
对于小型脚本或工具,直接用模块级变量最省事,也不需要理解复杂机制。中型项目推荐 __new__ 或装饰器,既能明确控制初始化,又不引入元类概念。大型框架级别代码如果单例类多,元类可以让约束统一,减少重复劳动。
无论选哪种方式,都要留意测试场景:单元测试里经常希望每个用例有干净环境,全局单例会导致状态串扰。可以在测试前重置实例属性,或把实例放在依赖注入容器里,按需替换,这样既能享受共享便利,也不牺牲可测性。
| 方案 | 实现难度 | 线程安全 | 适用场景 |
|---|---|---|---|
| 模块变量 | 低 | 天然安全 | 简单配置、工具脚本 |
| __new__ 方法 | 中 | 加锁后安全 | 单个核心类 |
| 元类 | 高 | 加锁后安全 | 多类需统一单例 |
| 装饰器 | 中 | 加锁后安全 | 灵活复用 |
总结
在 Python 项目中全局共享单个类实例,核心思路就是让实例化过程可控且只发生一次。语言层面的模块缓存、__new__ 拦截、元类接管和装饰器包装都能达成目标。实际落地时,结合团队熟悉度和业务复杂度来选,并处理好多线程与测试隔离,就能稳定地复用同一个对象。