单例模式练习

单例模式自测小练习——详细说明与参考答案

本文对《Python 设计模式——单例模式》文末的八、自测小练习做逐题详解,包括:题目在考什么、实现思路、完整代码和验证方式,并附带练习 3 的思考指引。


练习一:用 __new__ + _initialized 实现 Settings 单例

题目要求

写一个 Settings 类,用 __new__ 实现单例,并在 __init__ 里用 _initialized 保证只加载一次默认配置(例如一个字典)。

在考什么?

  • 能否用 __new__ 控制“只创建一次实例”。
  • 是否理解:单例时 __init__ 可能被多次调用,需要用标志位(如 _initialized)保证“默认配置”只加载一次。

实现思路

  1. 单例:类属性 _instance 存唯一实例;__new__ 里若 _instance is Nonesuper().__new__(cls) 并赋给 _instance,否则直接返回 _instance
  2. 只加载一次配置:在 __init__ 里先判断是否已初始化(例如 _initialized);若未初始化,则加载默认配置字典并置 _initialized = True,否则不做任何事。

参考答案

class Settings:
    _instance = None
    _initialized = False

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

    def __init__(self):
        if Settings._initialized:
            return
        Settings._initialized = True
        # 只执行一次的“默认配置”加载
        self.config = {
            "theme": "light",
            "language": "zh",
            "debug": False,
        }
        print("默认配置已加载(只执行一次)")

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

    def set(self, key, value):
        self.config[key] = value

# 验证单例 + 只加载一次配置
s1 = Settings()  # 输出:默认配置已加载(只执行一次)
s2 = Settings()  # 无输出

print(s1 is s2)           # True —— 是同一个对象
print(s1.get("theme"))    # light
s2.set("theme", "dark")
print(s1.get("theme"))    # dark —— 共用同一份 config

验证要点

  • s1 is s2True
  • “默认配置已加载”只打印一次。
  • 通过 s1s2 修改 config,另一边读取到的是同一份数据。

练习二:用装饰器把 Settings 改成单例并验证

题目要求

装饰器方式,把上面的 Settings 改成装饰器单例,并验证两次获取是同一对象

在考什么?

  • 能否写出一个“单例装饰器”:用字典保存“类 → 实例”,第一次创建后后续都返回同一实例。
  • 能否去掉 __new__ 单例逻辑,仅靠装饰器实现单例。
  • 验证时用 id()is 判断两次获取的是同一对象。

实现思路

  1. 定义装饰器函数 singleton(cls),内部维护 instances = {}
  2. 定义内层函数 get_instance(*args, **kwargs):若 cls not in instancesinstances[cls] = cls(*args, **kwargs),然后 return instances[cls]
  3. 返回 get_instance(注意返回的是函数,所以 Settings 名字实际指向的是该函数,调用 Settings() 即调用 get_instance())。
  4. 被装饰的类可以恢复成“普通类”,只负责业务(如 config 字典),单例逻辑完全由装饰器负责。

参考答案

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 Settings:
    def __init__(self):
        self.config = {
            "theme": "light",
            "language": "zh",
            "debug": False,
        }
        print("Settings 被创建")

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

    def set(self, key, value):
        self.config[key] = value

# 验证两次获取是同一对象
s1 = Settings()   # 输出:Settings 被创建
s2 = Settings()   # 无输出

print(s1 is s2)              # True
print(id(s1) == id(s2))      # True
print(s1.get("theme"))       # light
s2.set("theme", "dark")
print(s1.get("theme"))       # dark

验证要点

  • s1 is s2Trueid(s1) == id(s2)True
  • “Settings 被创建”只打印一次。
  • s1/s2config 修改是同一份数据。

练习三:思考项目里是否适合用单例

题目要求

思考:你当前项目里,有没有“全局只需要一个”的对象?它适合做成单例吗?

在考什么?

  • 能否把单例的适用场景(全局唯一、大家共用)和不适用场景(多实例、需要隔离、便于测试)区分开。
  • 能否在实际项目里识别候选对象,并判断是否“适合”单例(而不是滥用)。

思考指引(可写在笔记里)

  1. 先找“全局只需一个”的候选

    • 配置/环境变量读取器
    • 数据库连接池、Redis 客户端
    • 日志 Logger
    • 缓存管理器、线程池
    • 应用主窗口、全局事件总线等
  2. 再判断“是否适合单例”

    • 适合:确实全局共用、无多套配置/多连接池需求、创建成本高或状态需要集中管理。
    • 需谨慎:要方便单元测试时,单例的“全局唯一”会带来隐藏依赖,可考虑依赖注入或提供“测试用重置接口”。
    • 不适合:需要多个独立实例(如多个数据库连接、多套配置)、需要频繁 mock 替换的对象,用普通实例 + 注入更合适。
  3. 简单自问

    • “这个对象在整个程序里是否真的只能有一个?”
    • “如果做成单例,测试时会不会很难替换或清理?”
    • “用传参或依赖注入能不能更清晰?”

回答完以上,再决定是否用单例。

示例回答(可作模板)

  • 项目里有的对象:例如“从 config.json 读取的配置”。
  • 是否全局只需一个:是,所有模块读同一份配置。
  • 是否适合单例:适合,用单例或模块级变量都可以;若项目已有依赖注入容器,用“注入一个配置对象”也可以,不一定非要单例类。

小结

  • 练习一:掌握 __new__ 单例 + _initialized 保证只初始化一次,重点理解“单例时 __init__ 可能被多次调用”。
  • 练习二:掌握用装饰器把任意类变成单例,并会用 is / id 验证“两次获取是同一对象”。
  • 练习三:在项目里识别“全局只需一个”的对象,并判断是否适合单例,避免滥用。

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

发表评论