python设计模式–原型模式(Prototype Pattern)

Python 设计模式——原型模式(Prototype Pattern)详解

本文面向零基础新手,从概念到浅拷贝与深拷贝、从简单克隆到原型注册表,全方位讲解原型模式。


一、什么是原型模式?

1.1 通俗理解

原型模式是一种创建型设计模式,核心思想是:

不通过“new 类”来创建新对象,而是通过“复制已有对象”来创建。被复制的对象叫“原型”,复制出来的新对象可以再按需修改,从而避免重复执行昂贵的初始化逻辑。

可以这样类比:

  • 不用原型:每次要一份新试卷,老师都重新出题、排版、打印一遍(每次 new 对象并重新执行复杂初始化)。
  • 用原型:老师先做好一份“模板试卷”(原型),以后每次需要新试卷就复印这份模板,再在复印稿上改题号或微调(复制已有对象,再按需改)。复印比重新出题快得多。

所以,原型模式解决的是:当“创建新对象”成本高或想基于已有状态快速得到新对象时,用“克隆”代替“重新构造”。

1.2 为什么需要原型?

场景一:创建成本高

对象创建需要查数据库、读文件、算一大串、或连外部服务,你不想每次要“又一个类似对象”时都重来一遍。这时可以先有一个“样板对象”(原型),新对象通过复制它得到,再改少量字段即可。

场景二:想保留“当前状态”的副本

例如游戏里的“存档”:当前角色状态(血量、装备、位置)是一个复杂对象,存档 = 把这个对象复制一份保存起来,读档 = 再复制回当前。若不用复制,就要手动把每个字段抄一遍,容易漏、难维护。

场景三:构造过程复杂,但已有现成对象

例如界面里“复制一个控件”:控件有很多属性(样式、事件、子节点),重新用构造函数拼一遍很麻烦;若控件本身支持“克隆”,直接 clone 一份再改 id 或位置即可。

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

场景 说明
对象创建成本高 初始化要查库、算数、联网等,复制比重新构造更省。
需要基于现有状态生成新对象 如存档/读档、撤销/重做时的状态快照。
想隐藏具体类名 调用方只拿到“一个对象”,复制它得到新对象,不依赖具体类。
同一套配置要生成多份 如模板文档、默认配置,先有一个“原型”,再 clone 多份后各自修改。

简单记忆:要“照抄一个已有对象”或“基于已有对象快速得到副本”时,考虑原型模式。


二、核心:复制对象——浅拷贝与深拷贝

在实现原型模式之前,必须先搞清 Python 里的浅拷贝深拷贝,否则“复制”出来的对象可能和你预期不一致。

2.1 赋值不是复制

class Config:
    def __init__(self, data):
        self.data = data

a = Config({"key": "value"})
b = a   # 这是“赋值”,b 和 a 指向同一个对象!

b.data["key"] = "changed"
print(a.data["key"])  # changed —— 改 b 会把 a 也改了
print(a is b)         # True

结论b = a 只是多了一个名字指向同一块内存,没有新对象。要“克隆”必须用拷贝

2.2 浅拷贝(Shallow Copy)

浅拷贝:新对象本身是新的,但对象内部的子对象(如列表、字典、其他对象)仍然是和原对象共享的。

import copy

class Document:
    def __init__(self, title, items):
        self.title = title
        self.items = items  # 假设是列表

orig = Document("报告", ["第一章", "第二章"])
shallow = copy.copy(orig)   # 浅拷贝

print(orig is shallow)           # False —— 是两个不同的 Document 对象
print(orig.items is shallow.items)  # True —— 但 items 列表是同一个!

shallow.items.append("第三章")
print(orig.items)   # ['第一章', '第二章', '第三章'] —— 原对象被影响了

结论:浅拷贝只复制“第一层”;若属性是可变对象(列表、字典、自定义对象),修改拷贝件会影响到原型。适合“没有嵌套可变结构”或“本来就想共享内部引用”的情况。

2.3 深拷贝(Deep Copy)

深拷贝:新对象和内部所有层级的子对象都会递归复制,拷贝件和原型互不影响。

import copy

orig = Document("报告", ["第一章", "第二章"])
deep = copy.deepcopy(orig)

print(orig.items is deep.items)  # False —— 列表也被复制了

deep.items.append("第三章")
print(orig.items)   # ['第一章', '第二章'] —— 原对象不受影响
print(deep.items)   # ['第一章', '第二章', '第三章']

结论:原型模式里,若对象内部有可变嵌套结构(列表、字典、其他对象),通常要用深拷贝才能得到真正独立的副本。

2.4 小结:何时用浅拷贝 / 深拷贝

对象结构 建议
只有数字、字符串等不可变类型 浅拷贝即可(效果上像深拷贝)。
有列表、字典等可变属性 若希望副本和原型完全独立,用深拷贝。
有嵌套对象、循环引用 用深拷贝;若需自定义复制逻辑,可实现 __deepcopy__

三、用 copy 模块实现原型(clone 方法)

思路:给类增加一个“克隆”方法(如 clone()),内部用 copy.copycopy.deepcopy 生成副本并返回。调用方通过 obj.clone() 得到新对象,不关心具体类名。

3.1 示例:简单配置对象

import copy

class Config:
    def __init__(self, theme, font_size, options):
        self.theme = theme
        self.font_size = font_size
        self.options = options  # 字典,可变

    def clone(self):
        """原型模式:返回当前对象的一份深拷贝"""
        return copy.deepcopy(self)

# 使用
default = Config("dark", 14, {"auto_save": True, "language": "zh"})
config2 = default.clone()

config2.theme = "light"
config2.options["language"] = "en"
print(default.theme)              # dark —— 原型没变
print(default.options["language"])  # zh
print(config2.options["language"])  # en

要点:有可变属性 options,用 deepcopy 才能保证改 config2 不影响 default

3.2 示例:只有不可变属性时用浅拷贝即可

class SimpleConfig:
    def __init__(self, theme, font_size):
        self.theme = theme
        self.font_size = font_size

    def clone(self):
        return copy.copy(self)

a = SimpleConfig("dark", 14)
b = a.clone()
b.theme = "light"
print(a.theme)  # dark

四、自定义拷贝逻辑:copydeepcopy

有时你希望控制拷贝行为(例如某些属性不要复制、或要特殊初始化),可以让类实现 __copy__ 和/或 __deepcopy__,这样 copy.copy(obj)copy.deepcopy(obj) 会自动调用你的实现。

4.1 copy(浅拷贝时调用)

import copy

class Node:
    def __init__(self, value, parent=None):
        self.value = value
        self.parent = parent  # 父节点引用,复制时可能不想复制整棵树

    def __copy__(self):
        # 浅拷贝:只复制 value,parent 保持引用
        new_node = Node(self.value, self.parent)
        return new_node

    def __deepcopy__(self, memo):
        # 深拷贝:递归复制,用 memo 避免循环引用
        new_parent = copy.deepcopy(self.parent, memo) if self.parent else None
        return Node(self.value, new_parent)

4.2 在 clone() 里用 copy 模块

有了 __copy__/__deepcopy__,类的使用者直接写 copy.copy(obj)copy.deepcopy(obj) 就会走你的逻辑。在原型模式里,可以在 clone() 里统一调用:

class Node:
    # ... 同上 ...

    def clone(self):
        return copy.deepcopy(self)

五、原型注册表(Prototype Registry)

有时你有很多种“原型”,希望按名字类型取到原型再克隆,而不是在代码里写死具体类。这时可以做一个原型注册表:把多份原型对象存进字典,按 key 取出来再 clone。

5.1 示例:按名称获取并克隆

import copy

class Shape:
    def __init__(self, color, x, y):
        self.color = color
        self.x = x
        self.y = y

    def clone(self):
        return copy.deepcopy(self)

    def __str__(self):
        return f"{self.color} shape at ({self.x},{self.y})"

# 注册表:名字 -> 原型对象
registry = {}

def register(name, prototype):
    registry[name] = prototype

def clone_from_registry(name):
    if name not in registry:
        raise KeyError(f"未知原型: {name}")
    return registry[name].clone()

# 注册几种原型
register("red_circle", Shape("red", 0, 0))
register("blue_square", Shape("blue", 10, 10))

# 从注册表克隆
obj1 = clone_from_registry("red_circle")
obj1.x, obj1.y = 5, 5
print(obj1)  # red shape at (5,5)

obj2 = clone_from_registry("red_circle")
print(obj2)  # red shape at (0,0) —— 新对象,独立于 obj1 和原型

要点:调用方只依赖“名字”和“克隆接口”,不依赖具体类;新增原型只需往注册表里 register,符合开闭原则。

5.2 用类封装注册表

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()

registry = PrototypeRegistry()
registry.register("default_config", Config("dark", 14, {"lang": "zh"}))
c = registry.clone("default_config")

六、完整示例:游戏存档(状态快照)

需求:角色有血量、坐标、背包(列表);存档 = 把当前角色对象深拷贝一份保存;读档 = 用存档的副本恢复状态(或再拷贝一份给当前角色)。

import copy

class GameCharacter:
    def __init__(self, name, hp, x, y, inventory=None):
        self.name = name
        self.hp = hp
        self.x = x
        self.y = y
        self.inventory = inventory or []

    def clone(self):
        return copy.deepcopy(self)

    def __str__(self):
        return f"{self.name} HP:{self.hp} ({self.x},{self.y}) 背包:{self.inventory}"

# 当前角色
player = GameCharacter("勇者", 100, 10, 20, ["剑", "药"])

# 存档 = 克隆当前状态
save_slot = player.clone()
print(save_slot)  # 勇者 HP:100 (10,20) 背包:['剑', '药']

# 之后玩家移动、受伤、捡东西
player.hp -= 30
player.x, player.y = 50, 50
player.inventory.append("盾")

# 读档 = 用存档的副本恢复(这里简化为“用存档再克隆一份”当作当前状态)
player = save_slot.clone()
print(player)  # 勇者 HP:100 (10,20) 背包:['剑', '药'] —— 回到存档状态

要点:角色内部有可变列表 inventory,必须用 deepcopy,否则“存档”和“当前”会共享同一个列表。


七、原型模式 vs 其他创建型模式

对比 原型 工厂 建造者
得到新对象的方式 复制已有对象 根据类型/参数 new 分步组装后 build
重点 克隆、状态副本、降低创建成本 创建哪种产品、隐藏具体类 构建过程、可选参数多
典型 存档、模板配置、复制控件 多种支付方式、多种动物 复杂配置、电脑组装

简单记忆:工厂是“造哪一种”,建造者是“怎么一步步造”,原型是“照抄哪一个”。


八、常见问题与注意点

8.1 循环引用

若对象 A 引用 B,B 又引用 A,deepcopy 会用 memo 字典避免无限递归,一般不需要自己处理;只有自己手写递归复制时要注意把已复制对象放进 memo。

8.2 深拷贝的性能与不可复制对象

深拷贝要遍历整个对象图,对象很大或层级很深时可能较慢;有的对象(如文件句柄、数据库连接)不应被复制,可在 __deepcopy__ 里直接返回 self 或抛错。

8.3 可变默认参数与原型

和“原型”无直接关系,但新手常踩坑:不要用可变对象当默认参数。

# 错误示范
class Bad:
    def __init__(self, items=[]):  # 默认列表是共享的!
        self.items = items

# 正确:默认用 None,内部新建列表
class Good:
    def __init__(self, items=None):
        self.items = items if items is not None else []

8.4 何时用浅拷贝当时用深拷贝

再次强调:只要属性里有可变类型(列表、字典、其他可变对象),且希望副本和原型完全独立,就用 copy.deepcopy 或实现 __deepcopy__;否则修改副本会误改原型。


九、小结

  • 原型模式:通过复制已有对象(原型)来创建新对象,而不是重新 new;适合创建成本高、需要状态副本、或想按“模板”生成多份对象的场景。
  • 浅拷贝 vs 深拷贝:浅拷贝只复制第一层,内部可变对象仍共享;深拷贝递归复制,副本与原型独立。有嵌套可变结构时优先用深拷贝。
  • 实现方式:在类里提供 clone(),内部用 copy.copycopy.deepcopy;需要自定义时实现 __copy__/__deepcopy__
  • 原型注册表:把多份原型存进字典,按名称取原型再 clone,便于扩展、符合开闭原则。

建议先自己写一个带列表或字典属性的类,分别用 copy.copycopy.deepcopy 克隆并修改,观察对原对象的影响;再实现一个带 clone() 的配置类和一个简单的原型注册表,这样对原型模式会掌握得比较扎实。

发表评论