原型模式练习

原型模式练习——克隆对比、clone() 与原型注册表

本文按《Python 设计模式——原型模式》文末建议,完成三项练习:
① 带列表/字典的类用 copy.copycopy.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.tagsshallow.meta 后,orig.tagsorig.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"

运行后你会看到:改 deeptagsmeta 后,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

验证要点

  • defaultshortcutsoptions 不应被 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)

验证要点

  • c1c2 都是“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() 与注册表的效果。


自检清单

做完上述练习后,可以自问:

  1. 浅拷贝时,修改拷贝件内部的列表/字典,原对象会不会变?
  2. 深拷贝时,修改拷贝件的列表/字典,原对象会不会变?不会
  3. 配置类内部有列表或字典时,clone() 里应该用 copy.copy 还是 copy.deepcopy应使用 copy.deepcopy,才能得到完全独立的副本。
  4. 原型注册表里存的是“类”还是“对象”?对象(原型实例);按名字取到后再调用该对象的 clone() 得到新对象。

通过这三项练习和自检,对原型模式会掌握得比较扎实。

发表评论