python设计模式–单例模式(Singleton Pattern)

Python 设计模式——单例模式(Singleton Pattern)详解

本文面向零基础新手,从概念到实现、从原理到陷阱,全方位讲解单例模式。


一、什么是单例模式?

1.1 通俗理解

单例模式是一种创建型设计模式,它的核心思想是:

保证一个类在整个程序中只存在一个实例(对象),并且提供一个全局访问点来获取这个唯一实例。

可以这样类比:

  • 不是单例:每次你问“给我一个数据库连接”,就新建一个连接 → 可能产生成百上千个连接,浪费资源且难以管理。
  • 是单例:无论你问多少次“给我数据库连接”,得到的都是同一个连接 → 全局只有这一个,大家共用。

所以,单例模式解决的是:“某个类只需要一个实例” 的需求。

1.2 适用场景(什么时候用?)

适合使用单例的典型场景包括:

场景 说明
数据库连接池 整个应用共用一个连接池,避免重复创建。
配置管理器 全局配置只加载一份,到处读取同一份数据。
日志记录器 一个 Logger 实例统一写日志,避免多实例导致日志混乱。
线程池 一个应用通常只需要一个线程池。
缓存管理器 全局缓存只维护一份。
应用程序主窗口 某些桌面程序要求只弹出一个主窗口。

简单记忆:凡是“全局只需要一个”的东西,都可以考虑单例。

1.3 单例模式要达成的目标

  1. 唯一性:类只能被实例化一次,之后得到的都是同一个对象。
  2. 全局访问:在程序的任何地方都能用统一的方式拿到这个唯一实例。
  3. 延迟创建(可选):有的实现是“第一次用到时才创建”,节省启动时的资源。

二、从“普通类”到“单例类”

2.1 普通类:每次都会产生新对象

先看一个普通类,方便对比:

class DatabaseConnection:
    def __init__(self):
        print("创建了一个新的数据库连接")

# 每次 new 都会执行 __init__,产生新对象
conn1 = DatabaseConnection()  # 输出:创建了一个新的数据库连接
conn2 = DatabaseConnection()  # 输出:创建了一个新的数据库连接

print(conn1 is conn2)  # False —— 是两个不同的对象!

这里 conn1conn2两个不同的对象,不符合“全局唯一实例”的要求。

2.2 单例类:全局只有一个实例

单例模式要做的,就是:无论你调用多少次“创建”,得到的都是同一个对象

# 后面我们会实现 Singleton 类
conn1 = Singleton.get_instance()
conn2 = Singleton.get_instance()
print(conn1 is conn2)  # True —— 是同一个对象!

三、单例模式的多种实现方式

Python 里实现单例有很多种写法,从简单到严谨,下面逐一说明。


方式一:使用模块(最简单,Python 推荐)

在 Python 里,模块(.py 文件)天然就是单例:模块在程序中只会被导入一次,模块级变量只会有一份。

示例:用模块实现“配置管理器”单例

新建文件 config.py

# config.py
class _Config:
    def __init__(self):
        self.host = "localhost"
        self.port = 3306
        self.debug = True

# 模块级变量:整个程序只有这一份
config = _Config()

在别的文件中使用:

# main.py
from config import config

print(config.host)   # localhost
print(config.port)   # 3306

# 再次导入,得到的还是同一个 config
from config import config as c2
print(config is c2)  # True

优点:简单、符合 Python 习惯、线程安全(模块导入时是单线程的)。
缺点:单例是“模块级对象”,不是“类的唯一实例”这种形式,有些人更希望用类来封装。


方式二:重写 __new__ 方法(经典做法)

在 Python 中,创建对象时先调用 __new__,再调用 __init__
要控制“只创建一次实例”,就要在 __new__ 里做文章:第一次创建时正常 new,之后都返回已经创建好的那个实例。

示例:Logger 单例

class Logger:
    _instance = None  # 类属性,用来保存“唯一实例”

    def __new__(cls):
        if cls._instance is None:
            # 第一次:真正创建一个实例
            cls._instance = super().__new__(cls)
            print("Logger 首次被创建")
        # 之后:直接返回已有实例,不再 new 新的
        return cls._instance

    def __init__(self):
        # 注意:每次 get_instance 时可能仍会走到 __init__,所以避免在这里做“只做一次”的初始化
        self.logs = []

    def log(self, message):
        self.logs.append(message)
        print(f"[LOG] {message}")

# 使用
logger1 = Logger()  # 输出:Logger 首次被创建
logger2 = Logger()  # 无输出,没有再次创建

print(logger1 is logger2)  # True
logger1.log("第一条日志")
logger2.log("第二条日志")
print(logger1.logs)  # ['第一条日志', '第二条日志'] —— 共用同一份 logs

要点

  • __new__(cls) 负责“造对象”,返回的才是真正创建出来的实例。
  • 用类属性 _instance 存这个唯一实例;第一次为 Nonesuper().__new__(cls),否则直接返回 _instance
  • 注意:多次调用 Logger() 时,__init__ 可能被多次调用,所以“只做一次”的初始化最好放在 __new__ 里或单独方法里,而不是完全依赖 __init__

方式三:使用类方法 get_instance()(更清晰)

很多人习惯不直接 Logger(),而是通过 Logger.get_instance() 获取单例,这样语义更明确,也便于在 get_instance() 里做线程安全等处理。

示例:带 get_instance 的配置单例

class AppConfig:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        # 避免重复初始化:用标记位
        if not hasattr(self, "_initialized"):
            self._initialized = True
            self.settings = {"theme": "dark", "language": "zh"}

    @classmethod
    def get_instance(cls):
        return cls()  # 这里调用 cls() 会走 __new__ 和 __init__

    def get(self, key):
        return self.settings.get(key)

# 使用
config1 = AppConfig.get_instance()
config2 = AppConfig.get_instance()
print(config1 is config2)  # True
print(config1.get("theme"))  # dark

这里用 _initialized 保证 settings 只初始化一次,避免多次 __init__ 覆盖。


方式四:装饰器实现单例(复用性高)

用装饰器可以把“任意类”变成单例,而不必每个类都写 __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 DatabasePool:
    def __init__(self):
        print("数据库连接池被创建")
        self.connections = []

pool1 = DatabasePool()  # 输出:数据库连接池被创建
pool2 = DatabasePool()  # 无输出
print(pool1 is pool2)  # True

优点:任何类只要加 @singleton 就变成单例,代码复用性好。
缺点:装饰后 DatabasePool 实际是函数 get_instance,类型注解、继承等会有点别扭,需要知道这一点。


方式五:元类(Metaclass)实现单例(偏进阶)

元类是“创建类的类”。通过自定义元类,可以控制“类如何创建实例”,从而在底层保证单例。

示例:单例元类

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 CacheManager(metaclass=SingletonMeta):
    def __init__(self):
        print("CacheManager 被创建")
        self.cache = {}

c1 = CacheManager()  # 输出:CacheManager 被创建
c2 = CacheManager()  # 无输出
print(c1 is c2)  # True

适用:当你希望多个类都服从同一套“单例规则”时,可以统一指定同一个元类。
注意:元类理解成本较高,新手可以先掌握前几种。


四、完整示例:从需求到单例实现

需求:做一个全局的“应用配置”单例,只加载一次配置文件,到处读取。

class AppConfig:
    _instance = None
    _initialized = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if AppConfig._initialized:
            return
        AppConfig._initialized = True
        self._load_config()

    def _load_config(self):
        # 模拟从文件读取配置
        self.data = {
            "app_name": "我的应用",
            "version": "1.0",
            "debug": True,
        }
        print("配置已加载")

    def get(self, key):
        return self.data.get(key)

    @classmethod
    def get_instance(cls):
        return cls()

# 在多个模块中使用
config = AppConfig.get_instance()
print(config.get("app_name"))  # 我的应用

五、多线程下的单例(线程安全)

在有多线程时,如果两个线程“同时第一次”创建单例,可能会造出两个对象,破坏单例。所以需要加锁,保证“创建实例”这段代码同一时间只被一个线程执行。

示例:线程安全的单例

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        with cls._lock:  # 只允许一个线程进入
            if cls._instance is None:
                cls._instance = super().__new__(cls)
        return cls._instance

这样即使多线程同时调用,也只会有一个线程真正执行 super().__new__(cls),得到的就是唯一实例。


六、单例的常见陷阱与注意点

6.1 继承问题

子类继承单例类时,如果不特别处理,子类和父类可能共用同一个 _instance,导致子类也变成“只有一个实例”,且可能和父类混用。
若需要“每个子类各自一个单例”,可以在元类或 __new__ 里用子类作为 key 存实例(例如用字典 cls -> instance)。

6.2 多次调用 __init__

__new__ 做单例时,Python 可能仍会对同一实例多次调用 __init__。所以:

  • 不要在 __init__ 里做“只做一次”的重要初始化,或
  • __init__ 里用标志位(如 _initialized)判断,只执行一次。

6.3 测试困难

单例是全局状态,单元测试时可能互相影响。解决办法:

  • 提供一种“重置单例”的方式(如类方法 reset_instance(),仅用于测试),或
  • 通过依赖注入传入“配置/连接”对象,而不是在代码里直接拿全局单例,便于在测试中替换。

6.4 不要滥用单例

只有真正“全局只需一个”的才用单例,否则会带来隐藏依赖、难以测试等问题。能通过参数传递或依赖注入解决的,优先考虑后者。


七、小结:如何选实现方式?

方式 难度 适用
模块变量 最简单 配置、全局工具等,Python 中很常用
new + 类属性 简单 需要“类形式”的单例,且单线程或对线程安全要求不高
get_instance() 简单 希望接口明确,便于以后加锁或扩展
装饰器 中等 希望多个类都能快速变成单例
元类 偏难 多个类统一用一套单例逻辑
加锁的 new 中等 多线程环境必须保证只建一个实例

新手建议

  • 先掌握“模块单例”和“__new__ + 类属性”两种。
  • 需要线程安全时,在 __new__ 里加锁。
  • 理解“唯一实例”“全局访问”“只初始化一次”这三个目标,再根据项目选择实现方式。

八、自测小练习

  1. 写一个 Settings 类,用 __new__ 实现单例,并在 __init__ 里用 _initialized 保证只加载一次默认配置(例如一个字典)。
  2. 用装饰器方式,把上面的 Settings 改成装饰器单例,并验证两次获取是同一对象。
  3. 思考:你当前项目里,有没有“全局只需要一个”的对象?它适合做成单例吗?

做完以上三点,你对单例模式的理解就会比较扎实。
单例模式就讲到这里,建议多敲几遍示例代码,再结合小练习巩固。

发表评论