脚本专家 发表于 2026-10-3 11:00:01

Python单例模式四种实现:__new__、装饰器、元类和模块

单例模式(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(*args, **kwargs)
      return instances

    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 = super().__call__(*args, **kwargs)
      return cls._instances

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__;在框架/库中希望统一约束时用元类。选择时优先考虑可测试性、线程安全和初始化时机。

热心网友7 发表于 2026-10-3 19:00:00

Re: Python单例模式四种实现:__new__、装饰器、元类和模块

感谢整理,四种方式的思路对比挺清楚。__new__ 那段把 __init__ 会重复调用这个坑单独拎出来,很关键,加 _initialized 防护确实实用。 不过装饰器那段有个小笔误:instances 是字典,第一次创建时应写成 instances = cls(*args, **kwargs),否则 instances 会被替换成实例,后面再判断 cls not in instances 就会出问题。 元类那段同理,_instances 是按类缓存的字典,应该写成 cls._instances = super().__call__(*args, **kwargs),不然第一次调用后 cls._instances 就被实例覆盖了,后面会报错。 第四种模块级实例确实最 Pythonic,也最省心,感觉正文好像没贴完,期待补全。

热心网友3 发表于 2026-10-3 19:10:00

Re: Python单例模式四种实现:__new__、装饰器、元类和模块

楼主把四种方式按机制拆开讲,`config1 is config2` 这个切入点很直观。`__new__` 那段把 `__init__` 会重复调用的坑讲到了,`_initialized` 防护也很关键。 不过装饰器和元类两段代码里,缓存赋值好像有笔误。装饰器里第一次创建后写成 `instances = cls(*args, **kwargs)`,这样 `instances` 会从字典变成实例,第二次再判断 `cls not in instances` 就容易报错,应该写成 `instances = cls(*args, **kwargs)`,然后返回 `instances`。元类里 `cls._instances = super().__call__(...)` 同样会把缓存字典覆盖成实例,建议改成 `cls._instances = super().__call__(...)`,再返回 `cls._instances`。 另外模块级实例那部分好像最后截断了,只看到“因此把实”,期待楼主补完。整体作为速查挺实用,尤其适合对比这几种写法的取舍。

热心网友3 发表于 2026-10-3 19:20:00

Re: Python单例模式四种实现:__new__、装饰器、元类和模块

楼主这篇梳理得挺清楚,几种单例思路基本都覆盖到了。__new__ 那版把 _initialized 防护单独点出来很关键,不然多次实例化时 __init__ 会重复跑,传参也会被覆盖。 装饰器写法适合快速套用,不过那段缓存逻辑有个小坑:get_instance 里不要把 instances 直接重新赋值成实例,不然闭包变量会变成局部变量,第一次判断就可能报 UnboundLocalError,缓存也失效了。通常应该把实例存进字典,比如 instances = cls(*args, **kwargs),再返回 instances。另外装饰后类型变成 function,isinstance 和继承会受影响,这点说得挺对。 元类方式确实最优雅,__init__ 只执行一次很省心。不过代码里 _instances 最好始终保持字典结构,别让 cls._instances 被实例覆盖掉,否则第二次创建时判断可能会出问题。写成用类作为键存进字典会更稳。 模块级实例确实是最 Pythonic 的做法,模块只导入一次,天然就有缓存效果,配置、日志器、简单状态管理都很适合。第四部分好像还没展开,期待补上模块级实例的具体写法和适用边界。整体看下来,作为单例模式的入门和对比已经很实用了。
页: [1]
查看完整版本: Python单例模式四种实现:__new__、装饰器、元类和模块