查看: 319|回复: 0

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

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
单例模式(Singleton)用于保证一个类只有一个实例,并提供全局访问点。典型场景包括数据库连接池、配置管理器、日志器和应用状态管理器。Python 没有 Java 那样的私有构造约束,但可以通过 __new__、装饰器、元类或模块级实例实现。下面按实现机制拆解四种写法,并给出可直接运行的代码与适用场景。

先看一个直观需求:如果全局配置每个模块都创建新对象,就会出现配置不一致;单例下多次实例化应返回同一个对象,config1 is config2 为 True。

一、使用 __new__ 控制实例创建
__new__ 负责返回实例。做法是在类中保存 _instance,为空时调用 super().__new__(cls),并把 _initialized 设为 False。需要注意:__init__ 每次实例化都会调用,所以要用 _initialized 防止重复初始化。
  1. class Singleton:
  2.     """单例——通过__new__控制实例创建"""
  3.     _instance = None
  4.     def __new__(cls, *args, **kwargs):
  5.         if cls._instance is None:
  6.             cls._instance = super().__new__(cls)
  7.             cls._instance._initialized = False
  8.         return cls._instance
  9.     def __init__(self, value=None):
  10.         # __init__每次都会调用!需要防护
  11.         if self._initialized:
  12.             return
  13.         self.value = value
  14.         self._initialized = True
  15. # 测试
  16. s1 = Singleton("第一次")
  17. s2 = Singleton("第二次")
  18. print(s1 is s2)  # True —— 同一个实例
  19. print(s1.value)  # 第一次 —— __init__只执行了一次
  20. print(s2.value)  # 第一次
复制代码

这种方式简单直观,适合需要传参构造的单例,但必须处理 __init__ 重复执行。

二、使用装饰器缓存实例
装饰器用闭包和字典缓存已创建的对象,键是类本身。第一次调用时执行 cls(*args, **kwargs),后续直接返回缓存。
  1. def singleton(cls):
  2.     """单例装饰器——包装类"""
  3.     instances = {}
  4.     def get_instance(*args, **kwargs):
  5.         if cls not in instances:
  6.             instances[cls] = cls(*args, **kwargs)
  7.         return instances[cls]
  8.     return get_instance
  9. @singleton
  10. class Database:
  11.     """数据库连接——单例"""
  12.     def __init__(self, connection_string="default"):
  13.         self.connection_string = connection_string
  14.         print(f"创建数据库连接: {connection_string}")
  15. # 测试
  16. db1 = Database("mysql://localhost/mydb")
  17. db2 = Database("postgresql://localhost/testdb")
  18. print(db1 is db2)  # True
  19. print(db1.connection_string)  # mysql://localhost/mydb
  20. print(db2.connection_string)  # mysql://localhost/mydb
复制代码

注意其缺点:装饰后 Database 不再是类,而是函数。type(Database) 为 function,isinstance 检查不再适用。

三、使用元类做彻底的单例
元类继承 type 并重写 __call__。_instances 按类保存实例,只有类不在字典中时才调用 super().__call__ 创建,然后缓存。这样类定义时就决定单例行为,且 __init__ 只执行一次,无需 _initialized 防护。
  1. class SingletonMeta(type):
  2.     """单例元类——自动让类成为单例"""
  3.     _instances = {}
  4.     def __call__(cls, *args, **kwargs):
  5.         if cls not in cls._instances:
  6.             cls._instances[cls] = super().__call__(*args, **kwargs)
  7.         return cls._instances[cls]
  8. class Config(metaclass=SingletonMeta):
  9.     """配置管理器——单例(通过元类)"""
  10.     def __init__(self, debug=False):
  11.         # 不需要_initialized防护——元类控制__init__只调用一次
  12.         self.debug = debug
  13.         self.settings = {}
  14.         print(f"Config初始化: debug={debug}")
  15. # 测试
  16. c1 = Config(debug=True)
  17. c2 = Config(debug=False)
  18. print(c1 is c2)  # True
  19. print(c1.debug)  # True —— 第一次初始化的值
  20. print(c2.debug)  # True
复制代码

元类方式最优雅,适合框架或库中统一控制单例行为,代价是理解成本稍高。

四、使用模块级实例(Pythonic)
Python 模块本身在第一次导入时执行,之后缓存到 sys.modules。因此把实例放在模块级别,其他模块导入时拿到的是同一个对象,这是最 Pythonic 的单例方式。
  1. # ===== config.py =====
  2. class AppConfig:
  3.     def __init__(self):
  4.         self.debug = False
  5.         self.host = "localhost"
  6.         self.port = 8080
  7.     def set_debug(self, enabled):
  8.         self.debug = enabled
  9. # 在模块级别创建实例——这就是单例
  10. config = AppConfig()
  11. # ===== main.py =====
  12. # from config import config
  13. #
  14. # # 所有地方导入的都是同一个config对象
  15. # config.set_debug(True)
  16. #
  17. # # 另一个文件
  18. # from config import config
  19. # print(config.debug)  # True —— 同一个对象
复制代码

优点是简单、不需要特殊语法;缺点是导入时即创建,不能延迟创建,也无法在构造时传参。

五、实战一:线程安全的日志管理器
下面用元类单例实现全局唯一 Logger。内部用 threading.Lock 保护日志写入,_handlers 保存处理器,_level 保存级别。log 方法生成毫秒级时间戳,输出到所有 handler;没有 handler 时默认打印到控制台。
  1. import threading
  2. from datetime import datetime
  3. class Logger(metaclass=SingletonMeta):
  4.     """日志管理器——全局唯一"""
  5.     def __init__(self):
  6.         self._lock = threading.Lock()
  7.         self._handlers = []
  8.         self._level = "INFO"
  9.     def set_level(self, level):
  10.         self._level = level
  11.     def add_handler(self, handler):
  12.         self._handlers.append(handler)
  13.     def log(self, message, level="INFO"):
  14.         """线程安全的日志记录"""
  15.         with self._lock:
  16.             timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
  17.             log_entry = f"[{timestamp}] [{level}] {message}"
  18.             # 输出到所有处理器
  19.             for handler in self._handlers:
  20.                 handler(log_entry)
  21.             # 默认输出到控制台
  22.             if not self._handlers:
  23.                 print(log_entry)
  24. # 使用——在任何地方获取同一个Logger
  25. def some_function():
  26.     logger = Logger()
  27.     logger.log("函数执行中...")
  28. def another_function():
  29.     logger = Logger()
  30.     logger.log("另一个函数执行中...", level="WARNING")
  31. some_function()
  32. another_function()
复制代码

六、实战二:数据库连接池
连接池同样适合单例:全局唯一,避免重复创建池。下面用 max_connections 控制上限,_connections 保存已创建连接,_in_use 保存占用连接,Lock 保证线程安全。get_connection 在未达上限时创建连接,否则抛出 RuntimeError("连接池已满");release_connection 释放占用;status 返回总数、占用数和可用数。
  1. from threading import Lock
  2. class ConnectionPool(metaclass=SingletonMeta):
  3.     """数据库连接池——全局唯一"""
  4.     def __init__(self, max_connections=10):
  5.         self.max_connections = max_connections
  6.         self._connections = []
  7.         self._in_use = set()
  8.         self._lock = Lock()
  9.         print(f"连接池创建: 最大{max_connections}个连接")
  10.     def get_connection(self):
  11.         """获取一个连接"""
  12.         with self._lock:
  13.             if len(self._connections) < self.max_connections:
  14.                 conn = f"连接-{len(self._connections) + 1}"
  15.                 self._connections.append(conn)
  16.             else:
  17.                 raise RuntimeError("连接池已满")
  18.             self._in_use.add(conn)
  19.             return conn
  20.     def release_connection(self, conn):
  21.         """释放连接"""
  22.         with self._lock:
  23.             self._in_use.discard(conn)
  24.     def status(self):
  25.         return {
  26.             "total": len(self._connections),
  27.             "in_use": len(self._in_use),
  28.             "available": len(self._connections) - len(self._in_use),
  29.         }
  30. # 使用
  31. pool = ConnectionPool(max_connections=5)
  32. conn1 = pool.get_connection()
  33. conn2 = pool.get_connection()
  34. print(pool.status())  # {'total': 2, 'in_use': 2, 'available': 0}
  35. pool.release_connection(conn1)
  36. print(pool.status())  # {'total': 2, 'in_use': 1, 'available': 1}
复制代码

七、四种实现对比与选择
__new__:简单直观,需要防 __init__ 重复,适合要传参的单例。装饰器:写法简洁,但类型变成函数,isinstance 检查不适用。元类:最彻底,类定义时决定单例,稍复杂,适合框架或库。模块:最 Pythonic,简单且无需特殊语法,但不能延迟创建、不能传参构造。

日常推荐顺序:模块级实例最简单;需要传参或更精细控制时用 __new__;在框架/库中希望统一约束时用元类。选择时优先考虑可测试性、线程安全和初始化时机。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-3 13:40 , Processed in 0.028510 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部