TUTORIAL SERIES

Design Patterns in Python: Flyweight

Optimizing Memory Usage

Amir Lavasani
8 min readApr 15, 2024

Have you encountered recurring coding challenges? Imagine having a toolbox of tried-and-true solutions readily available. That’s precisely what design patterns provide. In this series, we’ll explore what these patterns are and how they can elevate your coding skills.

Understanding the Flyweight Pattern

What is the Flyweight Design Pattern?

The Flyweight Design Pattern is categorized under structural design patterns and is commonly used to manage objects that share similar or identical states.

The key idea behind the Flyweight pattern is to separate the intrinsic state (shared among multiple objects) from the extrinsic state (unique to each object).

By doing so, we can store the intrinsic state in a centralized location and share it among multiple objects, while the extrinsic state can be managed individually by each object.

Intrinsic vs. Extrinsic State

  1. Intrinsic State: This refers to constant data stored within an object, residing solely within the object itself and immutable by external entities. It remains consistent across different contexts and is shared among multiple objects.
  2. Extrinsic State: Conversely, the extrinsic state represents the variable data of an object, influenced or altered by external factors or objects. This state is dynamic and can vary between different instances of the same object.

The Flyweight pattern suggests separating extrinsic state from objects, preserving the intrinsic state for reuse across contexts. This minimizes object instances, as they mainly differ in intrinsic state, simplifying the system.

Dall-E generated image with the following concept: A large central node with cords extending outward to smaller nodes surrounding it, symbolizing shared resources distributed from a central source.

When to Use the Flyweight Pattern:

The Flyweight Pattern is suitable for the following scenarios:

  1. Large Number of Objects: When your application needs to create a large number of objects with shared intrinsic state.
  2. Memory Optimization: If memory optimization is a concern and you want to reduce the memory footprint by sharing common state among multiple objects.
  3. Immutable State: When the objects can be made immutable or the shared state can be safely shared among multiple instances without risk of modification.
  4. Performance Improvement: If performance improvement is desired by minimizing object creation and reducing redundant data storage.
  5. Extrinsic State Separation: When it’s feasible to separate the intrinsic state (shared among objects) from the extrinsic state (unique to each object) to optimize memory usage.

Practical Example: Game Enemy Character

We use the Flyweight pattern to optimize the management of game enemy characters. We define a Flyweight class for shared intrinsic data, create Concrete Flyweight classes for specific enemy types, and implement a Flyweight Factory for object management.

By separating intrinsic and extrinsic data, we optimize memory usage while allowing for individual customization during gameplay. This approach efficiently handles large numbers of characters, enhancing performance and scalability in game development.

Terminology and Key Components

Understanding the key components of the Flyweight pattern is essential for its effective implementation. Here are the crucial components involved:

  1. Flyweight: Contains the intrinsic state shared between multiple objects, with the same flyweight object usable in various contexts. It stores the intrinsic state and receives the extrinsic state from the context.
  2. Context: Holds the extrinsic state unique across all original objects. When paired with a flyweight object, it represents the full state of the original object.
  3. Client: Calculates or stores the extrinsic state of flyweights. It treats flyweights as template objects and configures them at runtime by passing contextual data into their methods.
  4. Flyweight Factory: Manages a pool of existing flyweights, handling their creation and reuse. Clients interact with the factory to obtain flyweight instances, passing intrinsic state for retrieval or creation.
Flyweight design pattern structure diagram. Image from refactoring.guru

Flyweight Implementation in Python

This implementation demonstrates the abstract version of the Flyweight design pattern, including the Flyweight interface, concrete flyweight classes, flyweight factory, and client class.

Step 1: Define the Flyweight interface

Define the Flyweight interface with an operation method.

from abc import ABC, abstractmethod

# Step 1: Define the Flyweight interface
class Flyweight(ABC):
"""
The Flyweight interface declares a method for accepting extrinsic state
and performing operations based on it.
"""

@abstractmethod
def operation(self, extrinsic_state):
"""
Operation method accepting extrinsic state as input.
"""
pass

Step 2: Create concrete flyweight classes

Create concrete flyweight classes implementing the interface and storing intrinsic state.

# Step 2: Create concrete flyweight classes
class ConcreteFlyweight(Flyweight):
"""
ConcreteFlyweight implements the Flyweight interface and stores intrinsic state.
"""

def __init__(self, intrinsic_state):
self._intrinsic_state = intrinsic_state

def operation(self, extrinsic_state):
return f"ConcreteFlyweight: Intrinsic State - {self._intrinsic_state}, Extrinsic State - {extrinsic_state}"

Step 3: Implement the Flyweight Factory

Implement a Flyweight Factory to manage flyweight objects and ensure their uniqueness.

# Step 3: Implement the Flyweight Factory
class FlyweightFactory:
"""
FlyweightFactory manages flyweight objects and ensures their uniqueness.
"""

_flyweights = {}

@staticmethod
def get_flyweight(key):
"""
Retrieve or create a flyweight object based on the provided key.
"""
if key not in FlyweightFactory._flyweights:
FlyweightFactory._flyweights[key] = ConcreteFlyweight(key)
return FlyweightFactory._flyweights[key]

Step 4: Define the client class

Define a client class representing objects that use flyweight objects.

# Step 4: Define the client class
class Client:
"""
Client class represents objects that use flyweight objects.
"""

def __init__(self, key):
self._flyweight = FlyweightFactory.get_flyweight(key)

def operation(self, extrinsic_state):
"""
Perform an operation using the flyweight object and extrinsic state.
"""
return self._flyweight.operation(extrinsic_state)

# Example usage
if __name__ == "__main__":
client1 = Client("shared")
client2 = Client("shared")
client3 = Client("unique")

print(client1.operation("state 1")) # Output: ConcreteFlyweight: Intrinsic State - shared, Extrinsic State - state 1
print(client2.operation("state 2")) # Output: ConcreteFlyweight: Intrinsic State - shared, Extrinsic State - state 2
print(client3.operation("state 3")) # Output: ConcreteFlyweight: Intrinsic State - unique, Extrinsic State - state 3

GitHub Repo 🎉

Explore all code examples and design pattern implementations on GitHub!

Practical Example: Game Enemy Character

Step 1: Define the Flyweight Interface

Define the abstract EnemyFlyweight interface with a render method.

from abc import ABC, abstractmethod
import random

# Step 1: Define the Flyweight interface
class EnemyFlyweight(ABC):
"""
Flyweight interface declares a method for accepting extrinsic state
and performing operations based on it.
"""

@abstractmethod
def render(self, position):
"""
Render method accepting extrinsic state as input.
"""
pass

Step 2: Create Concrete Flyweight Classes

Create concrete flyweight classes (e.g., EnemyTypeA, EnemyTypeB) implementing the interface and storing intrinsic data like texture.

# Step 2: Create concrete flyweight classes
class EnemyTypeA(EnemyFlyweight):
"""
ConcreteFlyweight class for enemy type A.
"""

def __init__(self, texture):
self._texture = texture

def render(self, position):
"""
Render enemy type A with given position.
"""
print(f"Rendering Enemy Type A at position {position} with texture: {self._texture}")

class EnemyTypeB(EnemyFlyweight):
"""
ConcreteFlyweight class for enemy type B.
"""

def __init__(self, texture):
self._texture = texture

def render(self, position):
"""
Render enemy type B with given position.
"""
print(f"Rendering Enemy Type B at position {position} with texture: {self._texture}")

Step 3: Implement the Flyweight Factory

Implement the EnemyFlyweightFactory to manage flyweight objects and ensure their uniqueness based on texture.

# Step 3: Implement the Flyweight Factory
class EnemyFlyweightFactory:
"""
FlyweightFactory manages flyweight objects and ensures their uniqueness.
"""

_flyweights = {}

@staticmethod
def get_flyweight(texture):
"""
Retrieve or create a flyweight object based on the provided texture.
"""
if texture not in EnemyFlyweightFactory._flyweights:
if random.randint(0, 1) == 0:
EnemyFlyweightFactory._flyweights[texture] = EnemyTypeA(texture)
else:
EnemyFlyweightFactory._flyweights[texture] = EnemyTypeB(texture)
return EnemyFlyweightFactory._flyweights[texture]

Step 4: Define the client class

Define the client class (e.g., GameEnvironment) representing objects that use flyweight objects. Instantiate the client class, add enemies with different textures and positions, and Render all enemies in the game environment using their respective flyweight objects.

# Step 4: Define the client class
class GameEnvironment:
"""
Client class represents objects that use flyweight objects.
"""

def __init__(self):
self._enemies = []

def add_enemy(self, texture, position):
"""
Add a new enemy to the game environment.
"""
flyweight = EnemyFlyweightFactory.get_flyweight(texture)
self._enemies.append((flyweight, position))

def render_enemies(self):
"""
Render all enemies in the game environment.
"""
for flyweight, position in self._enemies:
flyweight.render(position)

# Example usage
if __name__ == "__main__":
# Create game environment
game = GameEnvironment()

# Add enemies with different textures and positions
game.add_enemy("texture_a", (10, 20))
game.add_enemy("texture_b", (30, 40))
game.add_enemy("texture_a", (50, 60))
game.add_enemy("texture_b", (70, 80))

# Render all enemies
game.render_enemies()

Real-World Use Cases for Flyweight

  1. Graphics Processing Units (GPUs): GPU frameworks like OpenGL and DirectX use Flyweight to efficiently manage textures and vertex data.
  2. Python Interning: Python caches small integers and certain strings using Flyweight to optimize memory usage.
  3. Database Connection Pools: Connection pool implementations in frameworks like Apache DBCP and HikariCP use Flyweight to manage and reuse database connections efficiently.
  4. Object Pooling Libraries: Libraries like Apache Commons Pool and ObjectPool use Flyweight to pool and reuse objects, reducing object creation overhead.
  5. Caching Libraries: Caching frameworks like Ehcache and Redis use Flyweight to cache objects and reduce memory usage.

Flyweight Best Practices and Potential Drawbacks

Pros:

  1. RAM Savings: Significant RAM savings are possible, especially beneficial for programs with numerous similar objects.

Cons:

  1. CPU Overhead: Potential trade-off between RAM and CPU cycles, particularly if recalculating context data for flyweight methods.
  2. Code Complexity: Increased code complexity, potentially confusing for new team members due to the separation of entity states.

Flyweight’s Relations with Other Patterns

The Flyweight pattern shares similarities and differences with several other design patterns:

Flyweight and Composite

Flyweight can be implemented to represent shared leaf nodes of the Composite tree, saving RAM by reducing the memory footprint of common objects.

Flyweight vs. Facade

While Flyweight focuses on creating many small objects efficiently, Facade concentrates on encapsulating complex subsystems into a single simplified interface.

Flyweight vs. Singleton

Although Flyweight may resemble Singleton if all shared states are reduced to one flyweight object, there are key differences.

Flyweight can have multiple instances with different intrinsic states, while Singleton allows only one instance. Additionally, Singleton objects can be mutable, whereas Flyweight objects are immutable.

Conclusion

In conclusion, we’ve explored the Flyweight pattern, which optimizes memory usage by sharing common data among multiple objects. We discussed its key components, including the Flyweight interface, Concrete Flyweight classes, Flyweight Factory, and Client. Additionally, we highlighted the pros and cons of using the Flyweight pattern.

We compared the Flyweight pattern to other related patterns such as Composite, Facade, and Singleton, illustrating its unique characteristics and applications. Finally, we implemented a practical example of a gaming character using the Flyweight pattern, demonstrating its efficiency in managing large numbers of similar objects.

Keep coding and exploring new design patterns! 👩‍💻

Next on the Series 🚀

Read More 📜

The Series 🧭

Explore the GitHub Repo 🎉

References

  1. Design Patterns: Elements of Reusable Object-Oriented Software (Book)
  2. refactoring.guru Flyweight
  3. Head First Design Patterns (Book)
  4. Flyweight Design Pattern in Java
  5. The Flyweight Chronicles: How to Make Your Code Weightless!
  6. Flyweight Design Pattern
  7. Game Programming Patterns Flyweight

--

--

Amir Lavasani

I delve into machine learning 🤖 and software architecture 🏰 to enhance my expertise while sharing insights with Medium readers. 📃