Python 享元模式
享元模式是一种结构型设计模式,它通过共享对象来最大限度地减少内存使用和提高性能。简单来说,就是"共享元数据"的思想。
想象一下你去图书馆借书:如果每本书都被不同的人单独购买,图书馆就需要存储成千上万本相同的书。但实际上,图书馆只需要存储一本《Python 入门》,所有想读这本书的人都可以借阅同一本。享元模式就是这样的"图书馆",它管理着可共享的对象。
核心思想
享元模式将对象的状态分为两种:
- 内部状态(Intrinsic State):不变的、可共享的部分
- 外部状态(Extrinsic State):变化的、不可共享的部分
通过共享内部状态,避免创建大量相似对象,从而节省系统资源。
为什么需要享元模式
问题场景
假设我们正在开发一个文字处理器,需要渲染文档中的字符。如果每个字符都创建一个独立的对象:
实例
# 不好的实现:每个字符都是独立对象
class Character:
def __init__(self, char, font, size, color):
self.char = char # 字符
self.font = font # 字体
self.size = size # 字号
self.color = color # 颜色
def render(self, position):
print(f"在位置 {position} 渲染字符 '{self.char}'")
# 使用示例
char_a = Character('A', '宋体', 12, '黑色')
char_b = Character('B', '宋体', 12, '黑色')
char_a_another = Character('A', '宋体', 12, '黑色') # 重复创建相同的'A'
class Character:
def __init__(self, char, font, size, color):
self.char = char # 字符
self.font = font # 字体
self.size = size # 字号
self.color = color # 颜色
def render(self, position):
print(f"在位置 {position} 渲染字符 '{self.char}'")
# 使用示例
char_a = Character('A', '宋体', 12, '黑色')
char_b = Character('B', '宋体', 12, '黑色')
char_a_another = Character('A', '宋体', 12, '黑色') # 重复创建相同的'A'
这种实现方式的问题:
- 内存浪费:相同的字符被重复创建
- 性能低下:大量对象创建和销毁开销
- 难以维护:对象数量爆炸式增长
解决方案
使用享元模式,我们可以:
- 共享相同的字符对象
- 只存储一份内部状态(字符本身)
- 外部状态(位置)在渲染时传入
享元模式的实现
基本结构
让我们用文字处理器的例子来实现享元模式:
实例
from typing import Dict
# 享元类 - 存储内部状态
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # 内部状态:字符内容
self.font = font # 内部状态:字体
self.size = size # 内部状态:字号
self.color = color # 内部状态:颜色
def render(self, position: tuple):
"""渲染字符,position 是外部状态"""
x, y = position
print(f"在位置({x}, {y})渲染: 字符'{self.char}' "
f"[字体:{self.font}, 大小:{self.size}, 颜色:{self.color}]")
# 享元工厂 - 管理共享对象
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# 创建对象的唯一标识键
key = f"{char}_{font}_{size}_{color}"
# 如果对象不存在,则创建并缓存
if key not in cls._characters:
cls._characters[key] = CharacterFlyweight(char, font, size, color)
print(f"创建新字符对象: {key}")
else:
print(f"重用现有字符对象: {key}")
return cls._characters[key]
# 客户端类 - 使用享元对象
class TextDocument:
def __init__(self):
self.characters = [] # 存储字符和位置信息
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# 从工厂获取享元对象
character = CharacterFactory.get_character(char, font, size, color)
# 存储字符对象和外部状态(位置)
self.characters.append((character, position))
def render(self):
print("\n=== 开始渲染文档 ===")
for character, position in self.characters:
character.render(position)
print("=== 文档渲染完成 ===\n")
# 享元类 - 存储内部状态
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # 内部状态:字符内容
self.font = font # 内部状态:字体
self.size = size # 内部状态:字号
self.color = color # 内部状态:颜色
def render(self, position: tuple):
"""渲染字符,position 是外部状态"""
x, y = position
print(f"在位置({x}, {y})渲染: 字符'{self.char}' "
f"[字体:{self.font}, 大小:{self.size}, 颜色:{self.color}]")
# 享元工厂 - 管理共享对象
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# 创建对象的唯一标识键
key = f"{char}_{font}_{size}_{color}"
# 如果对象不存在,则创建并缓存
if key not in cls._characters:
cls._characters[key] = CharacterFlyweight(char, font, size, color)
print(f"创建新字符对象: {key}")
else:
print(f"重用现有字符对象: {key}")
return cls._characters[key]
# 客户端类 - 使用享元对象
class TextDocument:
def __init__(self):
self.characters = [] # 存储字符和位置信息
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# 从工厂获取享元对象
character = CharacterFactory.get_character(char, font, size, color)
# 存储字符对象和外部状态(位置)
self.characters.append((character, position))
def render(self):
print("\n=== 开始渲染文档 ===")
for character, position in self.characters:
character.render(position)
print("=== 文档渲染完成 ===\n")
使用示例
实例
# 创建文档
document = TextDocument()
# 添加字符到文档
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0)) # 重用 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # 不同颜色,创建新对象
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # 重用 'H'
# 渲染文档
document.render()
# 查看工厂中的对象数量
print(f"工厂中总共创建了 {len(CharacterFactory._characters)} 个字符对象")
document = TextDocument()
# 添加字符到文档
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0)) # 重用 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # 不同颜色,创建新对象
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # 重用 'H'
# 渲染文档
document.render()
# 查看工厂中的对象数量
print(f"工厂中总共创建了 {len(CharacterFactory._characters)} 个字符对象")
输出结果:
创建新字符对象: H_Arial_12_black 创建新字符对象: e_Arial_12_black 创建新字符对象: l_Arial_12_black 重用现有字符对象: l_Arial_12_black 创建新字符对象: o_Arial_12_black 创建新字符对象: !_Arial_12_red 重用现有字符对象: H_Arial_12_black === 开始渲染文档 === 在位置(0, 0)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black] 在位置(1, 0)渲染: 字符'e' [字体:Arial, 大小:12, 颜色:black] 在位置(2, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black] 在位置(3, 0)渲染: 字符'l' [字体:Arial, 大小:12, 颜色:black] 在位置(4, 0)渲染: 字符'o' [字体:Arial, 大小:12, 颜色:black] 在位置(5, 0)渲染: 字符'!' [字体:Arial, 大小:12, 颜色:red] 在位置(0, 1)渲染: 字符'H' [字体:Arial, 大小:12, 颜色:black] === 文档渲染完成 === 工厂中总共创建了 6 个字符对象
享元模式的核心组件
1. Flyweight(享元接口或抽象类)
定义享元对象的接口,通常包含操作外部状态的方法。
2. ConcreteFlyweight(具体享元类)
实现享元接口,存储内部状态。内部状态必须是不可变的。
3. FlyweightFactory(享元工厂)
创建和管理享元对象,确保正确地共享享元对象。
4. Client(客户端)
维护外部状态,在需要时向享元工厂请求享元对象。
更复杂的示例:游戏开发中的应用
让我们看一个游戏开发中的实际例子 - 树木渲染系统:
实例
from typing import Dict, List
from dataclasses import dataclass
@dataclass
class TreeType:
"""享元类 - 树的类型(内部状态)"""
name: str # 树种名称
texture: str # 纹理文件
color: str # 基础颜色
def render(self, x: int, y: int, height: int):
"""渲染树,位置和高度是外部状态"""
print(f"在({x}, {y})渲染{self.name}树,高度{height}米 "
f"[纹理:{self.texture}, 颜色:{self.color}]")
class TreeFactory:
"""享元工厂 - 管理树类型"""
_tree_types: Dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
key = f"{name}_{texture}_{color}"
if key not in cls._tree_types:
cls._tree_types[key] = TreeType(name, texture, color)
print(f"创建新的树类型: {name}")
return cls._tree_types[key]
@classmethod
def list_tree_types(cls):
print(f"\n当前共有 {len(cls._tree_types)} 种树类型:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""树对象 - 包含享元引用和外部状态"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # 外部状态:X坐标
self.y = y # 外部状态:Y坐标
self.height = height # 外部状态:高度
self.tree_type = tree_type # 享元对象引用
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""森林 - 客户端类"""
def __init__(self):
self.trees: List[Tree] = []
def plant_tree(self, x: int, y: int, height: int,
name: str, texture: str, color: str):
tree_type = TreeFactory.get_tree_type(name, texture, color)
tree = Tree(x, y, height, tree_type)
self.trees.append(tree)
def render(self):
print("\n=== 开始渲染森林 ===")
for tree in self.trees:
tree.render()
print("=== 森林渲染完成 ===")
# 使用示例
forest = Forest()
# 种植树木 - 相同类型的树会共享享元对象
forest.plant_tree(10, 20, 15, "松树", "pine_texture.png", "深绿色")
forest.plant_tree(30, 40, 12, "松树", "pine_texture.png", "深绿色") # 重用松树类型
forest.plant_tree(50, 60, 18, "橡树", "oak_texture.png", "浅绿色")
forest.plant_tree(70, 80, 20, "松树", "pine_texture.png", "深绿色") # 再次重用
forest.plant_tree(90, 100, 16, "枫树", "maple_texture.png", "红色")
# 渲染森林
forest.render()
# 查看树类型统计
TreeFactory.list_tree_types()
from dataclasses import dataclass
@dataclass
class TreeType:
"""享元类 - 树的类型(内部状态)"""
name: str # 树种名称
texture: str # 纹理文件
color: str # 基础颜色
def render(self, x: int, y: int, height: int):
"""渲染树,位置和高度是外部状态"""
print(f"在({x}, {y})渲染{self.name}树,高度{height}米 "
f"[纹理:{self.texture}, 颜色:{self.color}]")
class TreeFactory:
"""享元工厂 - 管理树类型"""
_tree_types: Dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
key = f"{name}_{texture}_{color}"
if key not in cls._tree_types:
cls._tree_types[key] = TreeType(name, texture, color)
print(f"创建新的树类型: {name}")
return cls._tree_types[key]
@classmethod
def list_tree_types(cls):
print(f"\n当前共有 {len(cls._tree_types)} 种树类型:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""树对象 - 包含享元引用和外部状态"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # 外部状态:X坐标
self.y = y # 外部状态:Y坐标
self.height = height # 外部状态:高度
self.tree_type = tree_type # 享元对象引用
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""森林 - 客户端类"""
def __init__(self):
self.trees: List[Tree] = []
def plant_tree(self, x: int, y: int, height: int,
name: str, texture: str, color: str):
tree_type = TreeFactory.get_tree_type(name, texture, color)
tree = Tree(x, y, height, tree_type)
self.trees.append(tree)
def render(self):
print("\n=== 开始渲染森林 ===")
for tree in self.trees:
tree.render()
print("=== 森林渲染完成 ===")
# 使用示例
forest = Forest()
# 种植树木 - 相同类型的树会共享享元对象
forest.plant_tree(10, 20, 15, "松树", "pine_texture.png", "深绿色")
forest.plant_tree(30, 40, 12, "松树", "pine_texture.png", "深绿色") # 重用松树类型
forest.plant_tree(50, 60, 18, "橡树", "oak_texture.png", "浅绿色")
forest.plant_tree(70, 80, 20, "松树", "pine_texture.png", "深绿色") # 再次重用
forest.plant_tree(90, 100, 16, "枫树", "maple_texture.png", "红色")
# 渲染森林
forest.render()
# 查看树类型统计
TreeFactory.list_tree_types()
享元模式的优缺点
优点
- 大幅减少内存使用:通过共享相似对象,显著降低内存占用
- 提高性能:减少对象创建和垃圾回收的开销
- 代码复用:相同的对象逻辑只需实现一次
- 易于扩展:新增享元类型不会影响现有代码
缺点
- 增加复杂性:需要区分内部状态和外部状态
- 线程安全问题:共享对象在多线程环境下需要额外处理
- 可能引入bug:如果错误地修改了内部状态,会影响所有使用者
- 不适用于所有场景:只有当对象真正可共享时才有效
适用场景
适合使用享元模式的场景
- 大量相似对象:系统需要创建大量相似对象时
- 内存敏感应用:移动设备、嵌入式系统等内存受限环境
- 缓存系统:需要缓存和重用对象时
- 游戏开发:渲染大量相同类型的游戏对象
- 文档处理:文字处理器、表格处理等
不适合使用的场景
- 对象差异很大:如果每个对象都有独特的状态
- 外部状态复杂:如果外部状态的管理比对象创建更复杂
- 性能要求不高:在内存充足且性能要求不高的场景
实践练习
练习 1:改进字符渲染系统
尝试改进我们之前的字符渲染系统,添加对粗体、斜体等样式的支持:
实例
# 你的改进代码在这里
class AdvancedCharacterFlyweight:
# 添加对粗体、斜体、下划线的支持
pass
# 测试你的实现
def test_advanced_system():
# 创建包含不同样式的文档
pass
class AdvancedCharacterFlyweight:
# 添加对粗体、斜体、下划线的支持
pass
# 测试你的实现
def test_advanced_system():
# 创建包含不同样式的文档
pass
练习 2:实现图标管理系统
设计一个图标管理系统,相同的图标在不同位置显示时共享同一个对象:
实例
class IconFlyweight:
# 存储图标文件路径、尺寸等内部状态
pass
class IconFactory:
# 管理图标享元对象
pass
class Application:
# 在界面不同位置显示图标
pass
# 存储图标文件路径、尺寸等内部状态
pass
class IconFactory:
# 管理图标享元对象
pass
class Application:
# 在界面不同位置显示图标
pass
总结
享元模式是一种强大的优化技术,它通过共享对象来减少资源消耗。关键要点:
- 区分状态:明确区分内部状态(可共享)和外部状态(不可共享)
- 使用工厂:通过工厂类管理享元对象的创建和共享
- 权衡利弊:在内存节省和代码复杂性之间找到平衡
- 适用场景:主要用于存在大量相似对象的场景
点我分享笔记