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

# Bad implementation: each character is an independent object
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

from typing import Dict

# 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

# Create document
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 typing import Dict, List
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

  1. Significantly reduce memory usage: significantly lower memory consumption by sharing similar objects
  2. Improve performance: reduce the overhead of object creation and garbage collection
  3. Code reuse: the same object logic only needs to be implemented once
  4. Easy to extend: adding new flyweight types does not affect existing code

Disadvantages

  1. Increases complexity: need to distinguish between intrinsic and extrinsic state
  2. Thread safety issues: shared objects require extra handling in multi-threaded environments
  3. May introduce bugs: if intrinsic state is mistakenly modified, it will affect all users
  4. Not applicable to all scenarios: Only effective when the object is truly shareable

Applicable Scenarios

Scenarios Suitable for the Flyweight Pattern

  1. Large numbers of similar objects: When the system needs to create a large number of similar objects
  2. Memory-sensitive applications: Memory-constrained environments such as mobile devices and embedded systems
  3. Cache systems: When objects need to be cached and reused
  4. Game development: Rendering a large number of game objects of the same type
  5. Document processing: Word processors, spreadsheets, etc.

Scenarios Unsuitable for Use

  1. Objects differ greatly: If every object has a unique state
  2. Complex external state: If managing external state is more complex than object creation
  3. 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

# Your improved code goes here
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

class IconFlyweight:
    # 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:

  1. Distinguish states: Clearly distinguish internal state (shareable) from external state (non-shareable)
  2. Use a factory: Manage the creation and sharing of flyweight objects through a factory class
  3. Weigh pros and cons: Find a balance between memory savings and code complexity
  4. Applicable scenarios: Mainly used in scenarios with a large number of similar objects
Other extensions