<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>design-patterns on Kshitij Patil</title><link>https://ksh.fyi/tags/design-patterns/</link><description>Recent content in design-patterns on Kshitij Patil</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 18 Oct 2026 00:00:00 +0530</lastBuildDate><atom:link href="https://ksh.fyi/tags/design-patterns/index.xml" rel="self" type="application/rss+xml"/><item><title>A million objects: make fewer of them</title><link>https://ksh.fyi/blogs/python-flyweight/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0530</pubDate><guid>https://ksh.fyi/blogs/python-flyweight/</guid><description>&lt;p&gt;&lt;a href="https://ksh.fyi/blogs/python-slots/"&gt;Part 1&lt;/a&gt; made each tree smaller. This part makes fewer
objects instead.&lt;/p&gt;
&lt;p&gt;You only need this when two things are true: you have a lot of objects, and
a big part of each one is the same as in many others. A million trees with
three species qualify. A thousand trees don&amp;rsquo;t, and neither do a million trees
that are all different.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;In a game, a tree also needs to look like something. Say the forest now comes
from a file, and every tree has a species with a colour and a texture:&lt;/p&gt;</description><content:encoded><![CDATA[<p><a href="/blogs/python-slots/">Part 1</a> made each tree smaller. This part makes fewer
objects instead.</p>
<p>You only need this when two things are true: you have a lot of objects, and
a big part of each one is the same as in many others. A million trees with
three species qualify. A thousand trees don&rsquo;t, and neither do a million trees
that are all different.</p>
<h2 id="the-problem">The problem</h2>
<p>In a game, a tree also needs to look like something. Say the forest now comes
from a file, and every tree has a species with a colour and a texture:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">x,y,age,species,r,g,b
</span></span><span class="line"><span class="cl">12,40,7,oak,34,139,34
</span></span><span class="line"><span class="cl">13,41,9,birch,240,240,220
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tree</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&#34;x&#34;</span><span class="p">,</span> <span class="s2">&#34;y&#34;</span><span class="p">,</span> <span class="s2">&#34;age&#34;</span><span class="p">,</span> <span class="s2">&#34;species&#34;</span><span class="p">,</span> <span class="s2">&#34;colour&#34;</span><span class="p">,</span> <span class="s2">&#34;texture&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load</span><span class="p">(</span><span class="n">row</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span><span class="p">,</span> <span class="n">species</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">row</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Tree</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">age</span><span class="p">),</span> <span class="n">species</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">r</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">g</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">b</span><span class="p">)),</span> <span class="n">load_texture</span><span class="p">(</span><span class="n">species</span><span class="p">))</span>
</span></span></code></pre></div><p><code>load_texture</code> stands in for reading an image file and returns 256 new bytes
on each call. There are only three species, but parsing makes a new string, a
new tuple and a new texture for every row. A million trees carry a million
copies of three things:</p>
<table>
	<thead>
			<tr>
					<th>one tree, 3.13</th>
					<th style="text-align: right">bytes</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Tree</code> (6 slots)</td>
					<td style="text-align: right">80</td>
			</tr>
			<tr>
					<td><code>species</code> string</td>
					<td style="text-align: right">45</td>
			</tr>
			<tr>
					<td><code>colour</code> tuple</td>
					<td style="text-align: right">64</td>
			</tr>
			<tr>
					<td><code>texture</code> bytes</td>
					<td style="text-align: right">289</td>
			</tr>
			<tr>
					<td><strong>total</strong></td>
					<td style="text-align: right"><strong>478</strong></td>
			</tr>
	</tbody>
</table>
<p>That&rsquo;s <strong>478 MB</strong> for the forest, and about 400 MB of it is duplicated species data.
(<code>x</code>, <code>y</code> and <code>age</code> are small ints, which CPython already shares, as Part 1
explained.)</p>
<h2 id="the-flyweight">The flyweight</h2>
<p>Split each tree&rsquo;s data in two:</p>
<ul>
<li><strong>intrinsic</strong>: the same for every tree of a species (name, colour, texture).
Store it once, in a <code>TreeType</code>, and share it.</li>
<li><strong>extrinsic</strong>: different for every tree (<code>x</code>, <code>y</code>, <code>age</code>). Keep it on the
tree, next to a pointer to its <code>TreeType</code>.</li>
</ul>
<p>A factory function makes sure each species gets exactly one <code>TreeType</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cache</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">slots</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TreeType</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">colour</span><span class="p">:</span> <span class="nb">tuple</span>
</span></span><span class="line"><span class="cl">    <span class="n">texture</span><span class="p">:</span> <span class="nb">bytes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@cache</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">tree_type</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">colour</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">TreeType</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">colour</span><span class="p">,</span> <span class="n">load_texture</span><span class="p">(</span><span class="n">name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Tree</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="vm">__slots__</span> <span class="o">=</span> <span class="p">(</span><span class="s2">&#34;x&#34;</span><span class="p">,</span> <span class="s2">&#34;y&#34;</span><span class="p">,</span> <span class="s2">&#34;age&#34;</span><span class="p">,</span> <span class="s2">&#34;type&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># __init__ sets the four slots</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load</span><span class="p">(</span><span class="n">row</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">age</span><span class="p">,</span> <span class="n">species</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">row</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Tree</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">age</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">tree_type</span><span class="p">(</span><span class="n">species</span><span class="p">,</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">r</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">g</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">b</span><span class="p">))))</span>
</span></span></code></pre></div><p><code>@cache</code> keeps a dict keyed on the arguments, so each species is built once
and every later call returns the same object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Tree ─┐
</span></span><span class="line"><span class="cl">Tree ─┼──▶ TreeType(&#34;oak&#34;,   (34, 139, 34),   &lt;256 B&gt;)
</span></span><span class="line"><span class="cl">Tree ─┘
</span></span><span class="line"><span class="cl">Tree ─┬──▶ TreeType(&#34;birch&#34;, (240, 240, 220), &lt;256 B&gt;)
</span></span><span class="line"><span class="cl">Tree ─┘
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">tree_type</span><span class="o">.</span><span class="n">cache_info</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">CacheInfo</span><span class="p">(</span><span class="n">hits</span><span class="o">=</span><span class="mi">999997</span><span class="p">,</span> <span class="n">misses</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">maxsize</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">currsize</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>
</span></span></code></pre></div><p>A tree is now <strong>64 B</strong>, down from 478: the forest takes 64 MB instead of 478,
<strong>7.5× less</strong>. That&rsquo;s only 8 B more than Part 1&rsquo;s three-slot tree. The 64 B
already includes the shared part: the three <code>TreeType</code>s and the cache take
about 1.8 KB in total, under 0.002 B per tree.</p>
<p>Two things keep it working:</p>
<ul>
<li><strong><code>frozen=True</code>.</strong> A shared object must never change: <code>tree.type.colour = ...</code> would recolour every oak in the forest. Frozen dataclasses raise on
assignment.</li>
<li><strong>One way in.</strong> Callers must never build a <code>TreeType</code> themselves; all of
them go through <code>tree_type()</code>, or the sharing silently stops.</li>
</ul>
<h2 id="where-youll-find-it">Where you&rsquo;ll find it</h2>
<ul>
<li><strong>Games.</strong> This forest is the textbook case. Thousands of trees share one
mesh and one set of textures, and each tree stores only its position, size
and rotation. GPUs support it directly: instanced rendering draws one mesh
many times in a single call, given just the per-tree data. Tile maps work the
same way, with each cell pointing at a shared terrain type such as grass or
water.</li>
<li><strong>Text editors.</strong> The original example in <em>Design Patterns</em>: one glyph
object per letter of the alphabet, not per letter in the document. Where a
letter sits comes from the text around it.</li>
<li><strong>pandas.</strong> <code>astype(&quot;category&quot;)</code> stores each distinct value once, plus a
small integer code per row.</li>
<li><strong>CPython.</strong> Small ints from -5 to 256, interned strings, <code>None</code>, <code>True</code> and
<code>False</code> exist once each and are shared by every use.</li>
</ul>
<h2 id="also-worth-knowing">Also worth knowing</h2>
<p><strong><code>lru_cache(maxsize=1024)</code></strong> for when the set of types is open-ended. <code>@cache</code>
keeps every type forever; an LRU cache keeps the most recent ones and drops
the rest. A dropped type isn&rsquo;t freed while trees still use it, so it costs
nothing to get wrong, only some sharing.</p>
<p><strong><code>weakref.WeakValueDictionary</code></strong> for when types should go away once no tree
uses them. It holds the types without keeping them alive. One catch, straight
from Part 1: a slots class has no <code>__weakref__</code> slot, so it can&rsquo;t be weakly
referenced.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="n">weakref</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="n">tree_type</span><span class="p">(</span><span class="s2">&#34;oak&#34;</span><span class="p">,</span> <span class="p">(</span><span class="mi">34</span><span class="p">,</span> <span class="mi">139</span><span class="p">,</span> <span class="mi">34</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="ne">TypeError</span><span class="p">:</span> <span class="n">cannot</span> <span class="n">create</span> <span class="n">weak</span> <span class="n">reference</span> <span class="n">to</span> <span class="s1">&#39;TreeType&#39;</span> <span class="nb">object</span>
</span></span></code></pre></div><p>Python 3.11+ fixes that with <code>@dataclass(frozen=True, slots=True, weakref_slot=True)</code>.</p>
<p><strong><code>sys.intern</code></strong> is a flyweight for strings. If your trees really did need
their own <code>species</code> string, <code>sys.intern(species)</code> would make every <code>&quot;oak&quot;</code> the
same object.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Flyweight_pattern">The flyweight pattern</a>, from <em>Design Patterns</em> (Gamma et al., 1994)</li>
<li>The <a href="https://gameprogrammingpatterns.com/flyweight.html">Flyweight chapter</a> of <em>Game Programming Patterns</em> (Robert Nystrom), which uses a forest too</li>
<li><a href="https://docs.python.org/3/library/functools.html#functools.cache"><code>functools.cache</code></a></li>
<li><a href="https://docs.python.org/3/library/dataclasses.html"><code>dataclasses</code></a>: <code>frozen</code>, <code>slots</code>, <code>weakref_slot</code></li>
<li><a href="https://docs.python.org/3/library/weakref.html#weakref.WeakValueDictionary"><code>weakref.WeakValueDictionary</code></a></li>
</ul>
]]></content:encoded></item></channel></rss>