Python 享元模式

Python 中的享元模式(Flyweight Pattern)

享元模式是一种结构型设计模式,其核心目的是:
通过共享大量细粒度的对象,来有效减少内存占用和对象创建开销

形象比喻:就像汉字印刷术中的“活字”——同一个字模(享元)可以被多次复用印刷不同页面,而不是每个页面都重新雕刻一个新字。

为什么需要享元模式?

当系统中需要创建大量相似对象时(例如:

  • 游戏中的树木、草地、粒子
  • 文字处理器中的每个字符
  • 图形编辑器中的大量相同形状
  • 棋盘游戏中的棋子

直接创建每个对象会导致内存爆炸。享元模式通过分离内在状态(共享)和外在状态(不共享)来解决这个问题。

  • 内在状态(Intrinsic State):对象内部不变、可以共享的部分(如字符的字体、大小、形状)
  • 外在状态(Extrinsic State):依赖上下文、不可共享的部分(如字符在文档中的位置、颜色)

Python 实现示例:文字处理器中的字符

我们实现一个简单的文字渲染系统,每个字符对象只存储内在状态(字符本身、字体),位置和颜色作为外在状态传入。

from typing import Dict

# 享元类(Flyweight)
class Character:
    def __init__(self, char: str, font_family: str, font_size: int):
        self.char = char                  # 内在状态
        self.font_family = font_family    # 内在状态
        self.font_size = font_size        # 内在状态

    def display(self, x: int, y: int, color: str):
        # 外在状态作为参数传入
        print(f"绘制字符 '{self.char}' "
              f"字体: {self.font_family} {self.font_size}pt "
              f"位置: ({x}, {y}) "
              f"颜色: {color}")

# 享元工厂(Flyweight Factory)—— 管理共享对象
class CharacterFactory:
    _characters: Dict[str, Character] = {}

    @classmethod
    def get_character(cls, char: str, font_family: str, font_size: int) -> Character:
        key = f"{char}_{font_family}_{font_size}"
        if key not in cls._characters:
            print(f"创建新享元对象: {key}")
            cls._characters[key] = Character(char, font_family, font_size)
        else:
            print(f"复用已有享元对象: {key}")
        return cls._characters[key]

    @classmethod
    def get_count(cls) -> int:
        return len(cls._characters)

# 客户端使用
if __name__ == "__main__":
    factory = CharacterFactory()

    # 模拟渲染一段文字:"Hello World" 使用相同字体
    text = "Hello World"
    font_family = "Arial"
    font_size = 12

    y = 100
    for i, char in enumerate(text):
        x = 50 + i * 20
        color = "black" if char != "o" else "red"  # 'o' 用红色突出

        character = factory.get_character(char, font_family, font_size)
        character.display(x, y, color)

    print(f"\n总共创建的享元对象数量: {factory.get_count()}")

输出

创建新享元对象: H_Arial_12
绘制字符 'H' 字体: Arial 12pt 位置: (50, 100) 颜色: black
创建新享元对象: e_Arial_12
绘制字符 'e' 字体: Arial 12pt 位置: (70, 100) 颜色: black
创建新享元对象: l_Arial_12
绘制字符 'l' 字体: Arial 12pt 位置: (90, 100) 颜色: black
复用已有享元对象: l_Arial_12
绘制字符 'l' 字体: Arial 12pt 位置: (110, 100) 颜色: black
复用已有享元对象: o_Arial_12
创建新享元对象: o_Arial_12
绘制字符 'o' 字体: Arial 12pt 位置: (130, 100) 颜色: red
...(后续复用已有对象)

总共创建的享元对象数量: 9  # 只有9种不同字符+字体组合,而不是12个独立对象

即使渲染了12个字符,但只创建了9个享元对象(H,e,l,o, ,W,r,d 各一个),大大节省内存!

更现实的例子:游戏中的树木

class TreeType:  # 享元
    def __init__(self, name: str, texture: str, height: int):
        self.name = name
        self.texture = texture
        self.height = height

    def render(self, x: int, y: int):
        print(f"渲染树: {self.name} 纹理:{self.texture} 高度:{self.height}m 位置:({x},{y})")

class TreeFactory:
    _tree_types: Dict[str, TreeType] = {}

    @classmethod
    def get_tree_type(cls, name, texture, height) -> TreeType:
        key = f"{name}_{texture}_{height}"
        if key not in cls._tree_types:
            cls._tree_types[key] = TreeType(name, texture, height)
        return cls._tree_types[key]

# 森林中成千上万棵树,只用几种树型
factory = TreeFactory()

# 只创建几种树型
oak = factory.get_tree_type("Oak", "oak_texture.png", 20)
pine = factory.get_tree_type("Pine", "pine_texture.png", 15)

# 但可以渲染成千上万棵树(只存位置作为外在状态)
trees = []
for i in range(1000):
    tree_type = oak if i % 2 == 0 else pine
    trees.append((tree_type, i * 10, i * 5))  # 只存位置

# 渲染时传入外在状态
for tree_type, x, y in trees[:5]:  # 只显示前5棵
    tree_type.render(x, y)

内存中只有2种树型对象,却可以渲染任意多棵树!

享元模式结构总结

角色说明
Flyweight共享对象(Character / TreeType)
ConcreteFlyweight具体享元(内在状态)
FlyweightFactory工厂,管理共享享元实例
Client持有享元引用 + 外在状态

享元模式 vs 其他模式对比

模式目的关键特点
享元共享细粒度对象,节省内存分离内在/外在状态
单例全局唯一实例一个类只有一个实例
缓存复用计算结果通常基于键值对
组合树形结构一致处理部分-整体层次

Python 中的实用建议

  • Python 的字符串、整数小对象(-5~256)本身就是享元(interning)。
  • 使用 dictlru_cache 或弱引用(weakref)实现工厂更高效。
  • 结合 dataclass(frozen=True) 可以轻松创建不可变享元。
  • 现代游戏引擎(如 Unity、Unreal)大量使用享元思想(Instancing)。
from dataclasses import dataclass

@dataclass(frozen=True)  # 自动 hashable,适合做 dict key
class FlyweightChar:
    char: str
    font: str
    size: int

注意事项

  • 享元对象通常应该是不可变的(避免共享状态被修改)
  • 外在状态必须由客户端管理
  • 适用于对象数量极大但种类有限的场景
  • 权衡内存节省 vs 工厂管理开销

享元模式是优化内存使用的强大工具,尤其在处理大量重复对象(如游戏、图形、文档系统)时非常有效。

如果你想看更高级的例子(如结合弱引用的享元工厂、游戏粒子系统、棋类游戏棋子实现),或者与其他模式结合使用,欢迎继续问!

文章已创建 3511

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部