YouTip LogoYouTip

Python Flyweight

Flyweight Pattern is a structural design pattern that minimizes memory usage and improves performance by sharing objects. Simply put, it embodies the idea of "sharing metadata".

Imagine going to a library to borrow books: if every book were purchased separately by different people, the library would need to store thousands of identical copies. But in reality, the library only needs to store one copy of "Python for Beginners", and everyone who wants to read it can borrow the same copy. The Flyweight Pattern is like this "library", managing shareable objects.

Core Idea

The Flyweight Pattern divides object state into two types:

  • Intrinsic State: The unchanging, shareable part
  • Extrinsic State: The changing, unshareable part

By sharing intrinsic state, it avoids creating large numbers of similar objects, thereby saving system resources.


Why We Need the Flyweight Pattern

Problem Scenario

Suppose we are developing a word processor that needs to render characters in a document. If each character creates an independent object:

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  # 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')  # Duplicate creation of same 'A'

Problems with this implementation:

  • Memory waste: identical characters are created repeatedly
  • Low performance: overhead of creating and destroying large numbers of objects
  • Difficult to maintain: explosive growth in object count

Solution

Using the Flyweight Pattern, we can:

  • Share identical character objects
  • Store only one copy of intrinsic state (the character itself)
  • Pass extrinsic state (position) at render time

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: size
        self.color = color  # intrinsic state: color

    def render(self, position: tuple):
        """Render character, position is extrinsic state"""
        x, y = position
        print(f"Render at position ({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:
        # Create unique identifier key for the object
        key = f"{char}_{font}_{size}_{color}"
        
        # If object doesn't exist, create and cache it
        if key not in cls._characters:
            cls._characters = CharacterFlyweight(char, font, size, color)
            print(f"Create new character object: {key}")
        else:
            print(f"Reuse existing character object: {key}")
        
        return cls._characters

# 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):
        # Get flyweight object from factory
        character = CharacterFactory.get_character(char, font, size, color)
        
        # Store character object and extrinsic state (position)
        self.characters.append((character, position))

    def render(self):
        print("\n=== Start rendering document ===")
        for character, position in self.characters:
            character.render(position)
        print("=== Document rendering complete ===\n")

Usage Example

Example

# Create document
document = TextDocument()

# Add characters 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 number of objects in factory
print(f"Total character objects created in factory: {len(CharacterFactory._characters)}")

Output:

Create new character object: H_Arial_12_black
Create new character object: e_Arial_12_black
Create new character object: l_Arial_12_black
Reuse existing character object: l_Arial_12_black
Create new character object: o_Arial_12_black
Create new character object: !_Arial_12_red
Reuse existing character object: H_Arial_12_black

=== Start rendering document ===
Render at position (0, 0): character 'H' [font:Arial, size:12, color:black]
Render at position (1, 0): character 'e' [font:Arial, size:12, color:black]
Render at position (2, 0): character 'l' [font:Arial, size:12, color:black]
Render at position (3, 0): character 'l' [font:Arial, size:12, color:black]
Render at position (4, 0): character 'o' [font:Arial, size:12, color:black]
Render at position (5, 0): character '!' [font:Arial, size:12, color:red]
Render at position (0, 1): character 'H' [font:Arial, size:12, color:black]
=== Document rendering complete ===

Total character objects created in factory: 6

Core Components of the Flyweight Pattern

1. Flyweight (Flyweight Interface or Abstract Class)

Defines the interface for flyweight objects, typically containing methods that operate on extrinsic state.

2. ConcreteFlyweight (Concrete Flyweight Class)

Implements the flyweight interface, storing intrinsic state. Intrinsic state must be immutable.

3. FlyweightFactory (Flyweight Factory)

Creates and manages flyweight objects, ensuring proper sharing of flyweight objects.

4. Client

Maintains extrinsic state, requesting flyweight objects from the flyweight factory when needed.


More Complex Example: Application in Game Development

Let's look at a practical example in 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 tree, position and height are extrinsic state"""
        print(f"Render {self.name} tree at ({x}, {y}), height {height}m "
              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 = TreeType(name, texture, color)
            print(f"Create new tree type: {name}")
        return cls._tree_types

    @classmethod
    def list_tree_types(cls):
        print(f"\nCurrently have {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 = []

    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=== Start rendering forest ===")
        for tree in self.trees:
            tree.render()
        print("=== Forest rendering complete ===")

# Usage example
forest = Forest()

# Plant trees - same type of trees will 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 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()

Advantages and Disadvantages of the Flyweight Pattern

Advantages

  1. Significantly reduces memory usage: dramatically lowers memory footprint by sharing similar objects
  2. Improves performance: reduces overhead of object creation and garbage collection
  3. Code reuse: identical object logic only needs to be implemented once
  4. Easy to extend: adding new flyweight types won't affect existing code

Disadvantages

  1. Increases complexity: need to distinguish between intrinsic and extrinsic state
  2. Thread safety issues: shared objects require additional handling in multi-threaded environments
  3. May introduce bugs: if intrinsic state is modified incorrectly, it affects all users
  4. Not applicable to all scenarios: only effective when objects are truly shareable

Applicable Scenarios

Scenarios Suitable for the Flyweight Pattern

  1. Large numbers of similar objects: when the system needs to create large numbers of similar objects
  2. Memory-sensitive applications: mobile devices, embedded systems, and other memory-constrained environments
  3. Caching systems: when caching and reusing objects is needed
  4. Game development: rendering large numbers of identical game objects
  5. Document processing: word processors, spreadsheet processing, etc.

Scenarios Not Suitable for the Flyweight Pattern

  1. Objects with large differences: if each object has unique state
  2. Complex extrinsic state: if managing extrinsic state is more complex than object creation
  3. Low performance requirements: in scenarios with sufficient memory and low performance requirements

Practical Exercises

Exercise 1: Improve the Character Rendering System

Try to improve our previous character rendering system by adding support for bold, italic, and other styles:

Example

# Your improved code here

class AdvancedCharacterFlyweight:
    # Add support for bold, italic, underline
    pass

# Test your implementation
def test_advanced_system():
    # Create document with 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 at different positions:

Example

class IconFlyweight:
    # Store icon file path, size, and other intrinsic state
    pass

class IconFactory:
    # Manage icon flyweight objects
    pass

class Application:
    # Display icons at different positions in 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 differentiate intrinsic state (shareable) and extrinsic state (unshareable)
  2. Use factory: manage flyweight object creation and sharing through a factory class
  3. Weigh trade-offs: find balance between memory savings and code complexity
  4. Applicable scenarios: mainly used in scenarios with large numbers of similar objects
← Python StrategyPython Bridge β†’