Python 单例模式

单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

核心概念

想象一下公司的 CEO 职位 - 整个公司只能有一个 CEO,无论哪个部门需要向 CEO 汇报,他们访问的都是同一个 CEO 对象。单例模式就是编程世界中的"CEO 职位管理机制"。

为什么需要单例模式

在实际开发中,有些对象我们只需要一个实例就足够了,比如:

  • 配置管理器:整个应用程序共享同一套配置
  • 数据库连接池:避免重复创建连接,节省资源
  • 日志记录器:确保所有日志都写入同一个文件
  • 缓存系统:统一管理缓存数据

单例模式的实现方法

Python 提供了多种实现单例模式的方式,让我们从简单到复杂逐一了解。

方法一:使用模块(最简单的方式)

Python 的模块本身就是天然的单例模式。当模块被导入时,它会被初始化一次,后续的导入都会使用同一个实例。

实例

# singleton_module.py
class DatabaseConnection:
    def __init__(self):
        self.connection_string = "database://localhost:5432/mydb"
        print("数据库连接已创建")
   
    def query(self, sql):
        return f"执行查询: {sql}"

# 创建单例实例
db_instance = DatabaseConnection()

# 在其他文件中使用:
# from singleton_module import db_instance
# result = db_instance.query("SELECT * FROM users")

方法二:使用 __new__ 方法

通过重写 __new__ 方法来控制实例的创建过程。

实例

class Singleton:
    _instance = None
   
    def __new__(cls, *args, **kwargs):
        # 如果实例不存在,则创建新实例
        if not cls._instance:
            cls._instance = super().__new__(cls)
            print("创建新的单例实例")
        else:
            print("返回已存在的单例实例")
        return cls._instance
   
    def __init__(self, name):
        # 注意:__init__ 每次都会被调用
        self.name = name
        print(f"初始化实例,名称: {name}")

# 测试代码
print("=== 测试单例模式 ===")
s1 = Singleton("第一个实例")
s2 = Singleton("第二个实例")

print(f"s1 的 ID: {id(s1)}")
print(f"s2 的 ID: {id(s2)}")
print(f"s1 和 s2 是同一个对象吗? {s1 is s2}")
print(f"s1 名称: {s1.name}")
print(f"s2 名称: {s2.name}")  # 注意:这里会显示"第二个实例"

输出结果:

=== 测试单例模式 ===
创建新的单例实例
初始化实例,名称: 第一个实例
返回已存在的单例实例
初始化实例,名称: 第二个实例
s1 的 ID: 140245678945600
s2 的 ID: 140245678945600
s1 和 s2 是同一个对象吗? True
s1 名称: 第二个实例
s2 名称: 第二个实例

方法三:使用装饰器

创建一个通用的单例装饰器,可以轻松地将任何类转换为单例。

实例

def singleton(cls):
    """单例装饰器"""
    instances = {}
   
    def get_instance(*args, **kwargs):
        # 如果该类还没有实例,则创建新实例
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
            print(f"创建 {cls.__name__} 的新实例")
        else:
            print(f"返回已存在的 {cls.__name__} 实例")
        return instances[cls]
   
    return get_instance

@singleton
class ConfigurationManager:
    def __init__(self):
        self.settings = {}
        self.load_default_settings()
   
    def load_default_settings(self):
        self.settings = {
            "app_name": "我的应用",
            "version": "1.0.0",
            "debug_mode": True
        }
   
    def get_setting(self, key):
        return self.settings.get(key)
   
    def set_setting(self, key, value):
        self.settings[key] = value

# 测试代码
print("\n=== 测试装饰器单例 ===")
config1 = ConfigurationManager()
config2 = ConfigurationManager()

config1.set_setting("theme", "dark")
print(f"config1 主题: {config1.get_setting('theme')}")
print(f"config2 主题: {config2.get_setting('theme')}")  # 两个实例共享同一配置

方法四:使用元类(高级用法)

元类可以控制类的创建过程,是实现单例模式的另一种强大方式。

实例

class SingletonMeta(type):
    """单例元类"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        # 如果该类还没有实例,则创建新实例
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
            print(f"元类:创建 {cls.__name__} 的新实例")
        else:
            print(f"元类:返回已存在的 {cls.__name__} 实例")
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self, log_file="app.log"):
        self.log_file = log_file
        self.logs = []
        print(f"日志器初始化,文件: {log_file}")
   
    def log(self, message):
        log_entry = f"[{self.get_timestamp()}] {message}"
        self.logs.append(log_entry)
        print(f"记录日志: {log_entry}")
        return log_entry
   
    def get_timestamp(self):
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
   
    def get_logs(self):
        return self.logs.copy()

# 测试代码
print("\n=== 测试元类单例 ===")
logger1 = Logger("application.log")
logger2 = Logger("different.log")  # 这个文件名会被忽略

logger1.log("系统启动")
logger2.log("用户登录")

print(f"logger1 日志数量: {len(logger1.get_logs())}")
print(f"logger2 日志数量: {len(logger2.get_logs())}")
print(f"是同一个日志器吗? {logger1 is logger2}")

单例模式的应用场景

让我们通过一个完整的示例来看看单例模式在实际项目中的应用。

实战案例:应用配置管理器

实例

class AppConfig:
    _instance = None
   
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
   
    def __init__(self):
        # 防止重复初始化
        if not self._initialized:
            self.config_data = {}
            self.load_config()
            self._initialized = True
   
    def load_config(self):
        """模拟从配置文件加载配置"""
        self.config_data = {
            "database": {
                "host": "localhost",
                "port": 5432,
                "name": "myapp_db"
            },
            "server": {
                "host": "0.0.0.0",
                "port": 8000
            },
            "features": {
                "cache_enabled": True,
                "debug_mode": False
            }
        }
        print("配置加载完成")
   
    def get(self, key_path, default=None):
        """通过路径获取配置值,如 'database.host'"""
        keys = key_path.split('.')
        value = self.config_data
       
        try:
            for key in keys:
                value = value[key]
            return value
        except (KeyError, TypeError):
            return default
   
    def set(self, key_path, value):
        """设置配置值"""
        keys = key_path.split('.')
        config = self.config_data
       
        # 遍历到最后一个键的前一个
        for key in keys[:-1]:
            if key not in config:
                config[key] = {}
            config = config[key]
       
        # 设置最终的值
        config[keys[-1]] = value
        print(f"配置已更新: {key_path} = {value}")

# 使用示例
def demonstrate_config_usage():
    print("\n=== 配置管理器使用演示 ===")
   
    # 在不同地方获取配置管理器实例
    config1 = AppConfig()
    config2 = AppConfig()
   
    print(f"是同一个配置管理器吗? {config1 is config2}")
   
    # 读取配置
    db_host = config1.get("database.host")
    server_port = config1.get("server.port")
    print(f"数据库主机: {db_host}")
    print(f"服务器端口: {server_port}")
   
    # 更新配置
    config2.set("database.host", "192.168.1.100")
    config2.set("features.debug_mode", True)
   
    # 验证配置同步
    print(f"config1 数据库主机: {config1.get('database.host')}")
    print(f"config1 调试模式: {config1.get('features.debug_mode')}")

# 运行演示
demonstrate_config_usage()

单例模式的注意事项

优点

  1. 资源节约:避免重复创建对象,节省内存和系统资源
  2. 数据一致性:所有客户端使用同一个实例,确保数据一致
  3. 全局访问:提供统一的访问点,便于管理

缺点

  1. 全局状态:单例对象持有全局状态,可能引起意外的副作用
  2. 测试困难:由于全局状态,单元测试可能变得复杂
  3. 违反单一职责:单例类既要管理自己的业务逻辑,又要控制实例化

最佳实践

实例

class ThreadSafeSingleton:
    """线程安全的单例模式"""
    _instance = None
    _lock = threading.Lock()
   
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # 双重检查锁定
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    print("创建线程安全的单例实例")
        return cls._instance
   
    def __init__(self):
        # 确保只初始化一次
        if not hasattr(self, '_initialized'):
            self.data = {}
            self._initialized = True

练习与思考

动手练习

  1. 实现一个缓存管理器

    • 创建一个单例的缓存类
    • 支持设置、获取、删除缓存项
    • 添加缓存过期时间功能
  2. 改进配置管理器

    • 添加从 JSON 文件加载配置的功能
    • 实现配置变更监听器
    • 添加配置验证机制

思考题

  1. 在什么情况下应该避免使用单例模式?
  2. 单例模式如何影响代码的可测试性?
  3. 在多线程环境中使用单例模式需要注意什么?

总结

单例模式是 Python 中非常重要的设计模式,它通过确保一个类只有一个实例,提供了对资源的统一管理。通过模块、__new__ 方法、装饰器和元类等多种实现方式,我们可以根据具体需求选择最适合的方法。

记住,虽然单例模式很实用,但也要谨慎使用,避免过度使用导致的代码耦合和测试困难。在实际项目中,合理运用单例模式可以大大提升代码的质量和可维护性。