Memory-Efficient Object Sharing with the Flyweight Pattern in Python
The Flyweight pattern optimizes memory consumption by sharing intrinsic data across multiple similar instances. Instead of allocating separate resources for every object, the pattern isolates state that varies between contexts from state that remains constant. This division allows a single shared instance to serve numerous references, drastically reducing overhead in systems that instantiate thousands of homogeneous items.
Core Components
- Flyweight Contract: Defines the operations that acccept external (extrinsic) state.
- Concrete Flyweight: Stores immutable, shared data (intrinsic state) and implements the rendering or processing logic.
- Flyweight Factory: Manages the lifecycle of shared instances. It maintains a lookup table to prevent duplicate creation and returns existing objects when matching parameters are requested.
- Context: Holds the variable state specific to a single usage and delegates shared operations to the associated flyweight.
Implementation Walkthrough
Consider a scenario where a mapping application places thousands of location markers. Each marker shares the same icon, color scheme, and size definitions, but differs in geographic coordinates and label text.
Step 1: Define the Shared Type
from dataclasses import dataclass
@dataclass(frozen=True)
class MarkerStyle:
icon_name: str
primary_color: str
scale: float
def render(self, position_x: float, position_y: float, label: str) -> None:
print(f"Drawing {self.icon_name} (color: {self.primary_color}) at ({position_x}, {position_y}) with label '{label}'")
Step 2: Build the Caching Factory
class StyleRegistry:
_catalog: dict[tuple, 'MarkerStyle'] = {}
@classmethod
def acquire(cls, icon: str, color: str, size: float) -> 'MarkerStyle':
lookup_key = (icon, color, size)
if lookup_key not in cls._catalog:
print(f"Instantiating new style for {icon}")
cls._catalog[lookup_key] = MarkerStyle(icon, color, size)
return cls._catalog[lookup_key]
Step 3: Contextualize with Extrinsic State
class LocationPin:
def __init__(self, lat: float, lng: float, title: str, style: 'MarkerStyle'):
self.latitude = lat
self.longitude = lng
self.caption = title
self.shared_style = style
def display_on_map(self) -> None:
self.shared_style.render(self.latitude, self.longitude, self.caption)
Step 4: Orchestrate and Render
class MapOverlay:
def __init__(self):
self.pins: list['LocationPin'] = []
def add_pin(self, lat: float, lng: float, title: str, icon: str, color: str, scale: float) -> None:
style_ref = StyleRegistry.acquire(icon, color, scale)
new_pin = LocationPin(lat, lng, title, style_ref)
self.pins.append(new_pin)
def refresh_viewport(self) -> None:
for pin in self.pins:
pin.display_on_map()
# Execution flow
if __name__ == "__main__":
viewport = MapOverlay()
viewport.add_pin(40.7128, -74.0060, "New York", "pin-red", "#FF0000", 1.0)
viewport.add_pin(40.7129, -74.0058, "Empire State", "pin-red", "#FF0000", 1.0)
viewport.add_pin(34.0522, -118.2437, "Los Angeles", "pin-blue", "#0000FF", 1.2)
viewport.refresh_viewport()
When to Apply the Pattern
- High Instance Counts: Suitable when an application generates numerous objects that would otherwise duplicate identical attributes in memory.
- Separable State: Works best when object properties can be cleanly split into immutable, shared values and mutable, instance-specific values.
- Resource-Constrained Environments: Essential for systems with strict memory limits, such as embedded controllers or mobile runtimes.
- Batch Rendering Pipelines: Common in game engines and UI frameworks where thousands of sprites or widgets share the same asset textures.
- Data Interning: Languages frequently use this technique for string literals or numeric primitives, storing only one copy of identical values in a global pool.
Optimizing Expensive Resource Loads
Beyond graphical elements, the pattern efficiently caches heavy data payloads, such as database records or API responses that rarely change.
class QueryCacheManager:
_pool: dict[str, dict] = {}
@staticmethod
def fetch_record(record_id: str) -> dict:
if record_id not in QueryCacheManager._pool:
# Simulate costly I/O operation
QueryCacheManager._pool[record_id] = {"id": record_id, "payload": "remote_data_loaded"}
print(f"Fetching record {record_id} from source.")
return QueryCacheManager._pool[record_id]
# Client usage
req1 = QueryCacheManager.fetch_record("USR_8842")
req2 = QueryCacheManager.fetch_record("USR_8842")
print(f"Memory address match: {req1 is req2}")