单例模式自测小练习——详细说明与参考答案
本文对《Python 设计模式——单例模式》文末的八、自测小练习做逐题详解,包括:题目在考什么、实现思路、完整代码和验证方式,并附带练习 3 的思考指引。
练习一:用 __new__ + _initialized 实现 Settings 单例
题目要求
写一个 Settings 类,用
__new__实现单例,并在__init__里用_initialized保证只加载一次默认配置(例如一个字典)。
在考什么?
- 能否用
__new__控制“只创建一次实例”。 - 是否理解:单例时
__init__可能被多次调用,需要用标志位(如_initialized)保证“默认配置”只加载一次。
实现思路
- 单例:类属性
_instance存唯一实例;__new__里若_instance is None则super().__new__(cls)并赋给_instance,否则直接返回_instance。 - 只加载一次配置:在
__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 s2为True。- “默认配置已加载”只打印一次。
- 通过
s1或s2修改config,另一边读取到的是同一份数据。
练习二:用装饰器把 Settings 改成单例并验证
题目要求
用装饰器方式,把上面的 Settings 改成装饰器单例,并验证两次获取是同一对象。
在考什么?
- 能否写出一个“单例装饰器”:用字典保存“类 → 实例”,第一次创建后后续都返回同一实例。
- 能否去掉
__new__单例逻辑,仅靠装饰器实现单例。 - 验证时用
id()或is判断两次获取的是同一对象。
实现思路
- 定义装饰器函数
singleton(cls),内部维护instances = {}。 - 定义内层函数
get_instance(*args, **kwargs):若cls not in instances则instances[cls] = cls(*args, **kwargs),然后return instances[cls]。 - 返回
get_instance(注意返回的是函数,所以Settings名字实际指向的是该函数,调用Settings()即调用get_instance())。 - 被装饰的类可以恢复成“普通类”,只负责业务(如
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 s2为True,id(s1) == id(s2)为True。- “Settings 被创建”只打印一次。
- 对
s1/s2的config修改是同一份数据。
练习三:思考项目里是否适合用单例
题目要求
思考:你当前项目里,有没有“全局只需要一个”的对象?它适合做成单例吗?
在考什么?
- 能否把单例的适用场景(全局唯一、大家共用)和不适用场景(多实例、需要隔离、便于测试)区分开。
- 能否在实际项目里识别候选对象,并判断是否“适合”单例(而不是滥用)。
思考指引(可写在笔记里)
-
先找“全局只需一个”的候选
- 配置/环境变量读取器
- 数据库连接池、Redis 客户端
- 日志 Logger
- 缓存管理器、线程池
- 应用主窗口、全局事件总线等
-
再判断“是否适合单例”
- 适合:确实全局共用、无多套配置/多连接池需求、创建成本高或状态需要集中管理。
- 需谨慎:要方便单元测试时,单例的“全局唯一”会带来隐藏依赖,可考虑依赖注入或提供“测试用重置接口”。
- 不适合:需要多个独立实例(如多个数据库连接、多套配置)、需要频繁 mock 替换的对象,用普通实例 + 注入更合适。
-
简单自问
- “这个对象在整个程序里是否真的只能有一个?”
- “如果做成单例,测试时会不会很难替换或清理?”
- “用传参或依赖注入能不能更清晰?”
回答完以上,再决定是否用单例。
示例回答(可作模板)
- 项目里有的对象:例如“从 config.json 读取的配置”。
- 是否全局只需一个:是,所有模块读同一份配置。
- 是否适合单例:适合,用单例或模块级变量都可以;若项目已有依赖注入容器,用“注入一个配置对象”也可以,不一定非要单例类。
小结
- 练习一:掌握
__new__单例 +_initialized保证只初始化一次,重点理解“单例时__init__可能被多次调用”。 - 练习二:掌握用装饰器把任意类变成单例,并会用
is/id验证“两次获取是同一对象”。 - 练习三:在项目里识别“全局只需一个”的对象,并判断是否适合单例,避免滥用。
做完以上三点,对单例模式的理解会比较扎实。建议多敲几遍示例代码,再结合小练习巩固。