19、设计模式:单例模式:模块级单例、装饰器实现、元类实现,确保全局唯一实例
单例模式,说白了就是——一个类,一辈子只有一个对象。
你可能会问:为什么非要搞唯一实例?
嗯,我举个例子你就明白了。我在做日志系统的时候,整个项目里所有模块都要往同一个日志文件里写。如果每个模块都 new 一个 Logger,那日志文件就乱套了,互相覆盖、重复打开句柄,最后排查问题你都不知道日志是谁写的。这时候,单例模式就派上用场了——全局只有一个 Logger 实例,谁用都是它。
今天咱们就聊三种 Python 里实现单例的常用手段:模块级单例、装饰器实现、元类实现。每种都有它的适用场景,我挨个给你拆开讲。
一、模块级单例:最 Pythonic 的方式
Python 的模块本身就是天然的单例。模块在第一次被导入时,会执行一次并生成一个 .pyc 文件,后续再导入,直接拿缓存。利用这个特性,我们可以把实例放在模块顶层。
我个人习惯用这种方式,因为它最简单,不需要额外写类或装饰器。
核心思路:在模块中直接创建实例,外部导入时拿到的永远是同一个对象。
# singleton_module.py
class DatabaseConnection:
def __init__(self):
self.host = "localhost"
self.port = 3306
print("数据库连接已创建")
# 模块级单例:直接在这里创建实例
db_instance = DatabaseConnection()
使用时,其他模块这样导入:
from singleton_module import db_instance
def query():
print(f"连接 {db_instance.host}:{db_instance.port}")
你想想看,不管多少个地方 from singleton_module import db_instance,拿到的都是同一个对象。因为模块只加载一次。
小提示:这种方式适合那些初始化开销大、且全局只需要一个实例的场景,比如数据库连接池、配置管理器。
不过要注意一点——如果你在模块里直接写 db_instance = DatabaseConnection(),那只要有人导入这个模块,实例就创建了。哪怕他根本没用。这叫「饿汉式」。如果你希望「懒加载」,那就得用下面两种方式了。
二、装饰器实现:优雅且可复用
装饰器实现单例,说白了就是用一个字典来缓存类和实例的映射关系。每次调用类的时候,先查字典,有就直接返回,没有就创建并缓存。
我在项目中遇到过这样一个场景:多个微服务共享同一个配置中心客户端。每个服务启动时都要初始化一次,但我不想让它们各自 new 一个。用装饰器,一行注解就搞定。
def singleton(cls):
"""单例装饰器"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class ConfigManager:
def __init__(self):
self.config = {}
print("配置管理器初始化")
使用的时候:
c1 = ConfigManager()
c2 = ConfigManager()
print(c1 is c2) # True
你看,c1 is c2 返回 True,说明是同一个对象。装饰器把原来的类替换成了一个函数,这个函数内部维护了一个字典 instances,闭包的特性保证了字典不会被回收。
避坑指南:我曾经在写多线程程序时,直接用这个装饰器,结果在高并发下出现了两个实例。为什么?因为 if cls not in instances 不是原子操作。解决方案是加锁,或者用 threading.Lock 包裹创建逻辑。
改进后的线程安全版本:
import threading
def singleton_threadsafe(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
嗯,这里用了双重检查锁定,既保证了线程安全,又避免了每次调用都加锁的性能损耗。
三、元类实现:最底层、最可控
元类,说白了就是「类的类」。普通类创建对象,元类创建类。通过控制元类的 __call__ 方法,我们可以在类被实例化时做手脚。
我记得有一次,我需要一个单例,但同时又希望这个类能正常继承、正常使用 isinstance 判断。装饰器方案会破坏类的类型信息(因为装饰器返回的是函数,不是类),而元类方案不会。
class SingletonMeta(type):
"""单例元类"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# 调用父类 type 的 __call__ 创建实例
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self):
self.log_file = "app.log"
print("日志器初始化")
使用:
l1 = Logger()
l2 = Logger()
print(l1 is l2) # True
print(type(l1)) # <class '__main__.Logger'>
你看,type(l1) 仍然是 Logger,而不是函数。这就是元类的优势——它不改变类的本质。
核心区别:装饰器把类变成了函数,元类保留了类的身份。如果你需要 isinstance 判断、需要继承,元类是更好的选择。
不过元类也有缺点:它比装饰器难理解,团队里新人可能看不懂。我个人建议,除非你有特殊需求(比如要保留类结构),否则优先用模块级单例或装饰器。
三种方式对比
| 实现方式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 模块级单例 | 最简单,零学习成本 | 饿汉式,无法懒加载 | 配置、日志、数据库连接 |
| 装饰器实现 | 优雅,可复用,支持懒加载 | 破坏类类型,多线程需加锁 | 工具类、客户端类 |
| 元类实现 | 保留类结构,支持继承 | 复杂度高,理解门槛高 | 框架底层、需要类型判断的场景 |
知识体系图
下面这张图帮你理清三种实现的核心逻辑和关系:
总结
单例模式的核心就一句话:确保一个类只有一个实例,并提供一个全局访问点。
三种实现方式各有千秋。我个人最常用的是模块级单例,因为它简单到不需要解释。但如果遇到需要懒加载或者多线程场景,我会毫不犹豫地选择装饰器加锁版本。至于元类,嗯,我一般只在写框架或库的时候才用,因为它对调用方最透明。
最后送你一句话:设计模式是工具,不是教条。别为了用模式而用模式,合适才是最好的。
避坑总结:我曾经在项目里看到有人用全局变量 + if not instance 的方式实现单例,结果在多线程下翻车了。记住,单例模式一定要考虑线程安全,除非你确定你的代码只在单线程中运行。