Python Flyweight Pattern
The Flyweight Pattern is a structural design pattern that minimizes memory usage and improves performance by sharing objects. In simple terms, it is the idea of "sharing metadata."
Imagine you borrow books from a library: if every book were purchased separately by different people, the library would need to store thousands of identical books. But in reality, the library only needs to store one copy of "Python Introduction", and everyone who wants to read it can borrow the same copy. The Flyweight Pattern is such a "library" that manages shareable objects.
Core Idea
The Flyweight Pattern divides the state of objects into two types:
- Intrinsic State: the immutable, shareable part
- Extrinsic State: the variable, non-shareable part
By sharing intrinsic state, it avoids creating a large number of similar objects, thus saving system resources.
Why Do We Need the Flyweight Pattern
Problem Scenario
Suppose we are developing a word processor and need to render characters in a document. If we create a separate object for each character:
Example
class Character:
def __init__(self, char, font, size, color):
self.char = char # character
self.font = font # font
self.size = size # font size
self.color = color # color
def render(self, position):
print(f"Render character '{self.char}' at position {position}")
# usage example
char_a = Character('A', 'SimSun', 12, 'black')
char_b = Character('B', 'SimSun', 12, 'black')
char_a_another = Character('A', 'SimSun', 12, 'black') # repeatedly creating the same 'A'
Problems with this implementation:
- Memory waste: the same characters are created repeatedly
- Poor performance: overhead from creating and destroying a large number of objects
- Hard to maintain: the number of objects explodes
Solution
Using the Flyweight pattern, we can:
- Share the same character objects
- Store only one copy of the intrinsic state (the character itself)
- Pass the extrinsic state (position) when rendering
Implementation of the Flyweight Pattern
Basic Structure
Let's implement the Flyweight pattern using the word processor example:
Example
# Flyweight class - stores intrinsic state
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # Intrinsic state: character content
self.font = font # Intrinsic state: font
self.size = size # Intrinsic state: font size
self.color = color # Intrinsic state: color
def render(self, position: tuple):
"""Render the character; position is the extrinsic state"""
x, y = position
print(f"Render at ({x}, {y}): character '{self.char}' "
f"[font:{self.font}, size:{self.size}, color:{self.color}]")
# Flyweight factory - manages shared objects
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# Unique identifier key for creating objects
key = f"{char}_{font}_{size}_{color}"
# If the object doesn't exist, create and cache it
if key not in cls._characters:
cls._characters[key] = CharacterFlyweight(char, font, size, color)
print(f"Creating new character object: {key}")
else:
print(f"Reusing existing character object: {key}")
return cls._characters[key]
# Client class - uses flyweight objects
class TextDocument:
def __init__(self):
self.characters = [] # Stores character and position information
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# Gets flyweight object from factory
character = CharacterFactory.get_character(char, font, size, color)
# Stores character object and extrinsic state (position)
self.characters.append((character, position))
def render(self):
print("\n"=== Starting to render document ===")
for character, position in self.characters:
character.render(position)
print("=== Document rendering complete ==="\n")
Usage Example
Example
document = TextDocument()
# Add character to document
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)) # Reuse 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # Different color, create new object
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # Reuse 'H'
# Render document
document.render()
# View the number of objects in the factory
print(f"Factory has created {len(CharacterFactory._characters)} character objects in total")
Output result:
创建新字符对象: 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 个字符对象
Core Components of the Flyweight Pattern
1. Flyweight (Flyweight Interface or Abstract Class)
Defines the interface for flyweight objects, usually containing methods for operating on extrinsic state.
2. ConcreteFlyweight (Concrete Flyweight Class)
Implements the flyweight interface and stores intrinsic state. The intrinsic state must be immutable.
3. FlyweightFactory (Flyweight Factory)
Creates and manages flyweight objects, ensuring flyweight objects are shared correctly.
4. Client
Maintains extrinsic state and requests flyweight objects from the flyweight factory when needed.
A More Complex Example: Application in Game Development
Let's look at a practical example from game development - a tree rendering system:
Example
from dataclasses import dataclass
@dataclass
class TreeType:
"""Flyweight class - tree type (intrinsic state)"""
name: str # Tree species name
texture: str # Texture file
color: str # Base color
def render(self, x: int, y: int, height: int):
"""Render the tree; position and height are extrinsic state"""
print(f"Render {self.name} tree at ({x}, {y}), height {height} meters "
f"[texture:{self.texture}, color:{self.color}]")
class TreeFactory:
"""Flyweight factory - manages tree types"""
_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"Creating new tree type: {name}")
return cls._tree_types[key]
@classmethod
def list_tree_types(cls):
print(f"\n"There are currently {len(cls._tree_types)} tree types:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""Tree object - contains flyweight reference and extrinsic state"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # Extrinsic state: X coordinate
self.y = y # Extrinsic state: Y coordinate
self.height = height # Extrinsic state: height
self.tree_type = tree_type # Flyweight object reference
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""Forest - client class"""
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"=== Starting to render forest ===")
for tree in self.trees:
tree.render()
print("=== Forest rendering complete ===")
# Usage example
forest = Forest()
# Plant trees - trees of the same type share flyweight objects
forest.plant_tree(10, 20, 15, "Pine", "pine_texture.png", "Dark Green")
forest.plant_tree(30, 40, 12, "Pine", "pine_texture.png", "Dark Green") # Reuse pine tree type
forest.plant_tree(50, 60, 18, "Oak", "oak_texture.png", "Light Green")
forest.plant_tree(70, 80, 20, "Pine", "pine_texture.png", "Dark Green") # Reuse again
forest.plant_tree(90, 100, 16, "Maple", "maple_texture.png", "Red")
# Render forest
forest.render()
# View tree type statistics
TreeFactory.list_tree_types()
Pros and Cons of the Flyweight Pattern
Advantages
- Significantly reduce memory usage: significantly lower memory consumption by sharing similar objects
- Improve performance: reduce the overhead of object creation and garbage collection
- Code reuse: the same object logic only needs to be implemented once
- Easy to extend: adding new flyweight types does not affect existing code
Disadvantages
- Increases complexity: need to distinguish between intrinsic and extrinsic state
- Thread safety issues: shared objects require extra handling in multi-threaded environments
- May introduce bugs: if intrinsic state is mistakenly modified, it will affect all users
- Not applicable to all scenarios: Only effective when the object is truly shareable
Applicable Scenarios
Scenarios Suitable for the Flyweight Pattern
- Large numbers of similar objects: When the system needs to create a large number of similar objects
- Memory-sensitive applications: Memory-constrained environments such as mobile devices and embedded systems
- Cache systems: When objects need to be cached and reused
- Game development: Rendering a large number of game objects of the same type
- Document processing: Word processors, spreadsheets, etc.
Scenarios Unsuitable for Use
- Objects differ greatly: If every object has a unique state
- Complex external state: If managing external state is more complex than object creation
- Low performance requirements: In scenarios with sufficient memory and low performance requirements
Practice Exercises
Exercise 1: Improve the Character Rendering System
Try to improve our previous character rendering system by adding support for styles such as bold and italic:
Example
class AdvancedCharacterFlyweight:
# Add support for bold, italic, and underline
pass
# Test your implementation
def test_advanced_system():
# Create a document containing different styles
pass
Exercise 2: Implement an Icon Management System
Design an icon management system where the same icon shares the same object when displayed in different positions:
Example
# Store internal state such as icon file path and dimensions
pass
class IconFactory:
# Manage icon flyweight objects
pass
class Application:
# Display icons in different positions on the interface
pass
Summary
The Flyweight pattern is a powerful optimization technique that reduces resource consumption by sharing objects. Key points:
- Distinguish states: Clearly distinguish internal state (shareable) from external state (non-shareable)
- Use a factory: Manage the creation and sharing of flyweight objects through a factory class
- Weigh pros and cons: Find a balance between memory savings and code complexity
- Applicable scenarios: Mainly used in scenarios with a large number of similar objects