原型模式练习——克隆对比、clone() 与原型注册表
本文按《Python 设计模式——原型模式》文末建议,完成三项练习:
① 带列表/字典的类用 copy.copy 与 copy.deepcopy 克隆并观察对原对象的影响;
② 实现带 clone() 的配置类;
③ 实现一个简单的原型注册表。
所有代码可直接复制运行。
练习一:带列表/字典的类——浅拷贝 vs 深拷贝
目的
体会:浅拷贝只复制“第一层”,内部的列表、字典仍是同一份,改拷贝会改到原对象;深拷贝会递归复制,副本和原对象完全独立。
定义一个带列表和字典属性的类
import copy
class DataHolder:
"""带列表和字典属性的类,用于观察浅拷贝与深拷贝的差异"""
def __init__(self, name, tags, meta):
self.name = name # 字符串,不可变
self.tags = tags # 列表,可变
self.meta = meta # 字典,可变
def __str__(self):
return f"DataHolder(name={self.name!r}, tags={self.tags}, meta={self.meta})"
1. 用 copy.copy(浅拷贝)克隆并修改
orig = DataHolder("原始", ["a", "b"], {"x": 1, "y": 2})
shallow = copy.copy(orig)
print("=== 浅拷贝 ===")
print("orig ", orig)
print("shallow", shallow)
print("orig is shallow? ", orig is shallow) # False,是两个对象
print("orig.tags is shallow.tags? ", orig.tags is shallow.tags) # True,同一列表
print("orig.meta is shallow.meta? ", orig.meta is shallow.meta) # True,同一字典
# 修改浅拷贝的“第一层”属性(不可变)——不影响原对象
shallow.name = "浅拷贝"
print("n修改 shallow.name 后:")
print("orig.name ", orig.name) # 原始
print("shallow.name", shallow.name) # 浅拷贝
# 修改浅拷贝内部的列表、字典——会影响到原对象!
shallow.tags.append("c")
shallow.meta["z"] = 3
print("n修改 shallow.tags 和 shallow.meta 后:")
print("orig ", orig) # tags 多了 "c",meta 多了 "z"
print("shallow", shallow)
运行后你会看到:改 shallow.tags 和 shallow.meta 后,orig.tags 和 orig.meta 也跟着变了,因为浅拷贝没有复制列表和字典,两边指向同一块内存。
2. 用 copy.deepcopy(深拷贝)克隆并修改
orig2 = DataHolder("原始", ["a", "b"], {"x": 1, "y": 2})
deep = copy.deepcopy(orig2)
print("n=== 深拷贝 ===")
print("orig2.tags is deep.tags? ", orig2.tags is deep.tags) # False
print("orig2.meta is deep.meta? ", orig2.meta is deep.meta) # False
# 修改深拷贝的列表、字典——原对象不受影响
deep.tags.append("c")
deep.meta["z"] = 3
print("n修改 deep.tags 和 deep.meta 后:")
print("orig2 ", orig2) # 仍是 ["a","b"] 和 {"x":1,"y":2}
print("deep ", deep) # 多了 "c" 和 "z"
运行后你会看到:改 deep 的 tags 和 meta 后,orig2 不变,因为深拷贝把内部结构也复制了一份。
3. 小结(练习一)
| 操作 | 浅拷贝 copy.copy | 深拷贝 copy.deepcopy |
|---|---|---|
| 对象本身 | 新对象 | 新对象 |
| 内部列表/字典 | 与原对象共享 | 全新复制,互不影响 |
| 修改拷贝的列表/字典 | 会改到原对象 | 不会影响原对象 |
结论:有列表、字典等可变属性且希望“副本和原型完全独立”时,应用 深拷贝 实现原型克隆。
练习二:带 clone() 的配置类
目的
在类里提供统一的 clone() 方法,内部用 copy.deepcopy 返回副本,调用方通过 obj.clone() 得到新对象,不依赖具体类名。
实现
import copy
class AppConfig:
"""带列表/字典的配置类,提供 clone() 实现原型模式"""
def __init__(self, theme, font_size, shortcuts, options):
self.theme = theme # 字符串
self.font_size = font_size # 数字
self.shortcuts = shortcuts # 列表,可变
self.options = options # 字典,可变
def clone(self):
"""返回当前对象的一份深拷贝,修改副本不影响原型"""
return copy.deepcopy(self)
def __str__(self):
return f"Config(theme={self.theme}, font_size={self.font_size}, shortcuts={self.shortcuts}, options={self.options})"
# 使用
default = AppConfig("dark", 14, ["Ctrl+S", "Ctrl+Q"], {"auto_save": True, "language": "zh"})
config2 = default.clone()
config2.theme = "light"
config2.font_size = 16
config2.shortcuts.append("Ctrl+N")
config2.options["language"] = "en"
print("default:", default)
print("config2:", config2)
# default 应保持原样,config2 的修改不会影响 default
验证要点:
default的shortcuts、options不应被config2的修改影响;config2是独立副本,可随意改。
练习三:简单的原型注册表
目的
维护一个“名字 → 原型对象”的注册表,通过名字取到原型再 clone(),得到新对象。调用方只依赖“名字”和“克隆”,不依赖具体类。
实现
import copy
class Config:
"""简化配置类,支持 clone()"""
def __init__(self, theme, options=None):
self.theme = theme
self.options = options if options is not None else {}
def clone(self):
return copy.deepcopy(self)
def __str__(self):
return f"Config(theme={self.theme}, options={self.options})"
class PrototypeRegistry:
"""简单原型注册表:按名称注册原型,按名称克隆"""
def __init__(self):
self._prototypes = {}
def register(self, name, prototype):
"""注册一个原型,prototype 需有 clone() 方法"""
self._prototypes[name] = prototype
def clone(self, name):
"""根据名称克隆一份原型,若名称不存在则抛 KeyError"""
if name not in self._prototypes:
raise KeyError(f"未知原型: {name}")
return self._prototypes[name].clone()
def list_names(self):
"""列出已注册的原型名称(便于调试)"""
return list(self._prototypes.keys())
# 使用
registry = PrototypeRegistry()
# 注册几种配置原型
registry.register("dark", Config("dark", {"sidebar": True}))
registry.register("light", Config("light", {"sidebar": False}))
registry.register("minimal", Config("minimal", {}))
print("已注册:", registry.list_names()) # ['dark', 'light', 'minimal']
# 从注册表克隆,得到独立对象
c1 = registry.clone("dark")
c1.options["font"] = "large"
print("c1:", c1)
c2 = registry.clone("dark")
print("c2:", c2) # 没有 font,因为 c2 是重新从原型 clone 的
# 再克隆 light
c3 = registry.clone("light")
print("c3:", c3)
验证要点:
c1和c2都是“dark”的副本,但彼此独立,改c1不影响c2也不影响注册表里的原型;- 通过名字即可获取并克隆,无需在业务代码里写死具体类。
完整可运行脚本(一次性跑完三个练习)
下面是一份完整脚本,可直接保存为 prototype_practice.py 运行,依次完成上述三个练习并打印对比结果。
# prototype_practice.py
import copy
# ---------- 练习一:浅拷贝 vs 深拷贝 ----------
class DataHolder:
def __init__(self, name, tags, meta):
self.name = name
self.tags = tags
self.meta = meta
def __str__(self):
return f"DataHolder(name={self.name!r}, tags={self.tags}, meta={self.meta})"
def practice1():
print("========== 练习一:浅拷贝 vs 深拷贝 ==========")
orig = DataHolder("原始", ["a", "b"], {"x": 1, "y": 2})
shallow = copy.copy(orig)
print("浅拷贝: orig.tags is shallow.tags?", orig.tags is shallow.tags)
shallow.tags.append("c")
shallow.meta["z"] = 3
print("修改 shallow 后 orig:", orig)
print("修改 shallow 后 shallow:", shallow)
orig2 = DataHolder("原始", ["a", "b"], {"x": 1, "y": 2})
deep = copy.deepcopy(orig2)
print("n深拷贝: orig2.tags is deep.tags?", orig2.tags is deep.tags)
deep.tags.append("c")
deep.meta["z"] = 3
print("修改 deep 后 orig2:", orig2)
print("修改 deep 后 deep:", deep)
# ---------- 练习二:带 clone() 的配置类 ----------
class AppConfig:
def __init__(self, theme, font_size, shortcuts, options):
self.theme = theme
self.font_size = font_size
self.shortcuts = shortcuts
self.options = options
def clone(self):
return copy.deepcopy(self)
def __str__(self):
return f"Config(theme={self.theme}, font_size={self.font_size}, shortcuts={self.shortcuts}, options={self.options})"
def practice2():
print("n========== 练习二:带 clone() 的配置类 ==========")
default = AppConfig("dark", 14, ["Ctrl+S"], {"language": "zh"})
config2 = default.clone()
config2.theme = "light"
config2.shortcuts.append("Ctrl+N")
config2.options["language"] = "en"
print("default:", default)
print("config2:", config2)
# ---------- 练习三:原型注册表 ----------
class Config:
def __init__(self, theme, options=None):
self.theme = theme
self.options = options if options is not None else {}
def clone(self):
return copy.deepcopy(self)
def __str__(self):
return f"Config(theme={self.theme}, options={self.options})"
class PrototypeRegistry:
def __init__(self):
self._prototypes = {}
def register(self, name, prototype):
self._prototypes[name] = prototype
def clone(self, name):
if name not in self._prototypes:
raise KeyError(f"未知原型: {name}")
return self._prototypes[name].clone()
def list_names(self):
return list(self._prototypes.keys())
def practice3():
print("n========== 练习三:原型注册表 ==========")
registry = PrototypeRegistry()
registry.register("dark", Config("dark", {"sidebar": True}))
registry.register("light", Config("light", {}))
print("已注册:", registry.list_names())
c1 = registry.clone("dark")
c1.options["font"] = "large"
c2 = registry.clone("dark")
print("c1:", c1)
print("c2:", c2)
if __name__ == "__main__":
practice1()
practice2()
practice3()
print("n全部练习完成。")
在项目目录下执行:
python prototype_practice.py
即可看到三个练习的对比输出,便于观察浅拷贝/深拷贝对原对象的影响,以及 clone() 与注册表的效果。
自检清单
做完上述练习后,可以自问:
- 浅拷贝时,修改拷贝件内部的列表/字典,原对象会不会变?会。
- 深拷贝时,修改拷贝件的列表/字典,原对象会不会变?不会。
- 配置类内部有列表或字典时,
clone()里应该用copy.copy还是copy.deepcopy?应使用copy.deepcopy,才能得到完全独立的副本。 - 原型注册表里存的是“类”还是“对象”?对象(原型实例);按名字取到后再调用该对象的
clone()得到新对象。
通过这三项练习和自检,对原型模式会掌握得比较扎实。