单例模式(Singleton)用于保证一个类只有一个实例,并提供全局访问点。典型场景包括数据库连接池、配置管理器、日志器和应用状态管理器。Python 没有 Java 那样的私有构造约束,但可以通过 __new__、装饰器、元类或模块级实例实现。下面按实现机制拆解四种写法,并给出可直接运行的代码与适用场景。
先看一个直观需求:如果全局配置每个模块都创建新对象,就会出现配置不一致;单例下多次实例化应返回同一个对象,config1 is config2 为 True。
一、使用 __new__ 控制实例创建
__new__ 负责返回实例。做法是在类中保存 _instance,为空时调用 super().__new__(cls),并把 _initialized 设为 False。需要注意:__init__ 每次实例化都会调用,所以要用 _initialized 防止重复初始化。
- class Singleton:
- """单例——通过__new__控制实例创建"""
- _instance = None
- def __new__(cls, *args, **kwargs):
- if cls._instance is None:
- cls._instance = super().__new__(cls)
- cls._instance._initialized = False
- return cls._instance
- def __init__(self, value=None):
- # __init__每次都会调用!需要防护
- if self._initialized:
- return
- self.value = value
- self._initialized = True
- # 测试
- s1 = Singleton("第一次")
- s2 = Singleton("第二次")
- print(s1 is s2) # True —— 同一个实例
- print(s1.value) # 第一次 —— __init__只执行了一次
- print(s2.value) # 第一次
复制代码
这种方式简单直观,适合需要传参构造的单例,但必须处理 __init__ 重复执行。
二、使用装饰器缓存实例
装饰器用闭包和字典缓存已创建的对象,键是类本身。第一次调用时执行 cls(*args, **kwargs),后续直接返回缓存。
- 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 Database:
- """数据库连接——单例"""
- def __init__(self, connection_string="default"):
- self.connection_string = connection_string
- print(f"创建数据库连接: {connection_string}")
- # 测试
- db1 = Database("mysql://localhost/mydb")
- db2 = Database("postgresql://localhost/testdb")
- print(db1 is db2) # True
- print(db1.connection_string) # mysql://localhost/mydb
- print(db2.connection_string) # mysql://localhost/mydb
复制代码
注意其缺点:装饰后 Database 不再是类,而是函数。type(Database) 为 function,isinstance 检查不再适用。
三、使用元类做彻底的单例
元类继承 type 并重写 __call__。_instances 按类保存实例,只有类不在字典中时才调用 super().__call__ 创建,然后缓存。这样类定义时就决定单例行为,且 __init__ 只执行一次,无需 _initialized 防护。
- class SingletonMeta(type):
- """单例元类——自动让类成为单例"""
- _instances = {}
- def __call__(cls, *args, **kwargs):
- if cls not in cls._instances:
- cls._instances[cls] = super().__call__(*args, **kwargs)
- return cls._instances[cls]
- class Config(metaclass=SingletonMeta):
- """配置管理器——单例(通过元类)"""
- def __init__(self, debug=False):
- # 不需要_initialized防护——元类控制__init__只调用一次
- self.debug = debug
- self.settings = {}
- print(f"Config初始化: debug={debug}")
- # 测试
- c1 = Config(debug=True)
- c2 = Config(debug=False)
- print(c1 is c2) # True
- print(c1.debug) # True —— 第一次初始化的值
- print(c2.debug) # True
复制代码
元类方式最优雅,适合框架或库中统一控制单例行为,代价是理解成本稍高。
四、使用模块级实例(Pythonic)
Python 模块本身在第一次导入时执行,之后缓存到 sys.modules。因此把实例放在模块级别,其他模块导入时拿到的是同一个对象,这是最 Pythonic 的单例方式。
- # ===== config.py =====
- class AppConfig:
- def __init__(self):
- self.debug = False
- self.host = "localhost"
- self.port = 8080
- def set_debug(self, enabled):
- self.debug = enabled
- # 在模块级别创建实例——这就是单例
- config = AppConfig()
- # ===== main.py =====
- # from config import config
- #
- # # 所有地方导入的都是同一个config对象
- # config.set_debug(True)
- #
- # # 另一个文件
- # from config import config
- # print(config.debug) # True —— 同一个对象
复制代码
优点是简单、不需要特殊语法;缺点是导入时即创建,不能延迟创建,也无法在构造时传参。
五、实战一:线程安全的日志管理器
下面用元类单例实现全局唯一 Logger。内部用 threading.Lock 保护日志写入,_handlers 保存处理器,_level 保存级别。log 方法生成毫秒级时间戳,输出到所有 handler;没有 handler 时默认打印到控制台。
- import threading
- from datetime import datetime
- class Logger(metaclass=SingletonMeta):
- """日志管理器——全局唯一"""
- def __init__(self):
- self._lock = threading.Lock()
- self._handlers = []
- self._level = "INFO"
- def set_level(self, level):
- self._level = level
- def add_handler(self, handler):
- self._handlers.append(handler)
- def log(self, message, level="INFO"):
- """线程安全的日志记录"""
- with self._lock:
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
- log_entry = f"[{timestamp}] [{level}] {message}"
- # 输出到所有处理器
- for handler in self._handlers:
- handler(log_entry)
- # 默认输出到控制台
- if not self._handlers:
- print(log_entry)
- # 使用——在任何地方获取同一个Logger
- def some_function():
- logger = Logger()
- logger.log("函数执行中...")
- def another_function():
- logger = Logger()
- logger.log("另一个函数执行中...", level="WARNING")
- some_function()
- another_function()
复制代码
六、实战二:数据库连接池
连接池同样适合单例:全局唯一,避免重复创建池。下面用 max_connections 控制上限,_connections 保存已创建连接,_in_use 保存占用连接,Lock 保证线程安全。get_connection 在未达上限时创建连接,否则抛出 RuntimeError("连接池已满");release_connection 释放占用;status 返回总数、占用数和可用数。
- from threading import Lock
- class ConnectionPool(metaclass=SingletonMeta):
- """数据库连接池——全局唯一"""
- def __init__(self, max_connections=10):
- self.max_connections = max_connections
- self._connections = []
- self._in_use = set()
- self._lock = Lock()
- print(f"连接池创建: 最大{max_connections}个连接")
- def get_connection(self):
- """获取一个连接"""
- with self._lock:
- if len(self._connections) < self.max_connections:
- conn = f"连接-{len(self._connections) + 1}"
- self._connections.append(conn)
- else:
- raise RuntimeError("连接池已满")
- self._in_use.add(conn)
- return conn
- def release_connection(self, conn):
- """释放连接"""
- with self._lock:
- self._in_use.discard(conn)
- def status(self):
- return {
- "total": len(self._connections),
- "in_use": len(self._in_use),
- "available": len(self._connections) - len(self._in_use),
- }
- # 使用
- pool = ConnectionPool(max_connections=5)
- conn1 = pool.get_connection()
- conn2 = pool.get_connection()
- print(pool.status()) # {'total': 2, 'in_use': 2, 'available': 0}
- pool.release_connection(conn1)
- print(pool.status()) # {'total': 2, 'in_use': 1, 'available': 1}
复制代码
七、四种实现对比与选择
__new__:简单直观,需要防 __init__ 重复,适合要传参的单例。装饰器:写法简洁,但类型变成函数,isinstance 检查不适用。元类:最彻底,类定义时决定单例,稍复杂,适合框架或库。模块:最 Pythonic,简单且无需特殊语法,但不能延迟创建、不能传参构造。
日常推荐顺序:模块级实例最简单;需要传参或更精细控制时用 __new__;在框架/库中希望统一约束时用元类。选择时优先考虑可测试性、线程安全和初始化时机。 |