Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Memory-Efficient Object Sharing with the Flyweight Pattern in Python

Notes 1

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}")

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.