<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Dict on KbWen Blog</title>
    <link>https://www.kbwen.com/tags/dict/</link>
    <description>Articles and notes on AI systems, machine learning, Python, data engineering, and software development.</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <image>
      <url>https://www.kbwen.com/images/og-default.png</url>
      <title>KbWen Blog</title>
      <link>https://www.kbwen.com/</link>
    </image>
    
    <lastBuildDate>Tue, 20 Oct 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://www.kbwen.com/tags/dict/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Python dict 的優化</title>
      <link>https://www.kbwen.com/python-dict-insertion-order/</link>
      <pubDate>Tue, 20 Oct 2026 10:00:00 +0800</pubDate><dc:creator>KbWen</dc:creator>
      <guid>https://www.kbwen.com/python-dict-insertion-order/</guid>
      <description>從 Python 3.6 dict 的改寫，來看看插入順序如何跟著記憶體優化。</description>
      <content:encoded><![CDATA[<blockquote>
<p>Python 3.6 重寫了 dict 的內部結構，記憶體用量比 3.5 少了 20% 到 25%，到 3.7，這個順序也從實作細節寫進規格，變成可以放心的依賴。</p>
</blockquote>
<p>現在寫 Python，把幾個值丟進去 dict、再用 for 迴圈跑一遍，出來的順序就是當初塞進去的順序。但3.5 以前還不是這樣：dict 的迭代順序並沒有任何意義，同一份程式換機器、換版本，順序就可能不一樣。那這個轉變是從 3.6 改版開始的。</p>
<h2 id="36-換掉了-dict-的內部結構">3.6 換掉了 dict 的內部結構</h2>
<p>3.6 改版，原始的動機是記憶體。它把 dict 換成一種比較緊湊的排法，官方的 What&rsquo;s New 上寫著，新的 dict 記憶體用量比 Python 3.5 少了 20% 到 25%。那至於我們開頭說的「照插入順序」這個效果：只算實作細節，不建議依賴，以後也可能會改變。這個順序在當時，也只是順著新結構冒出來的新東西。</p>
<h2 id="一次查詢兩層陣列">一次查詢、兩層陣列</h2>
<p>省記憶體跟保留順序，是同一個結構的兩面。以目前 CPython 的原始碼看，dict 內部拆成兩塊陣列。第一塊 indices，是真正的雜湊表，稀疏、有很多空格子；不過是很小的整數，代表數值存在哪裡。整數指向的是第二塊陣列 entries，key跟值一組一組密集地存在那裡。</p>
<p>查詢的時候，會先算雜湊、落到 indices 的某一格，讀出裡面的整數，再拿著它到 entries，取出真正的值。多繞一層看起來好像很麻煩，得到的是很明確的省空間：indices 每格只要放一個整數，dict 小的時候用 int8 就夠，大一點才需要換 int16、int32，每格頂多幾個 byte；佔空間的指標全集中在 entries，不必為了雜湊表的稀疏跟著留一堆空位。</p>
<p>那順序就是在這裡產生的。entries 會往後增加，新的鍵值只會加在最後，不會插進中間，所以如果此時從頭讀取 entries，先後就是當初的順序。原始碼註解點出的也是這件事：因為 entries 幾乎只增不改，要拿插入順序，把 entries 跑一遍就好。indices 負責雜湊定位，順序則是 entries 這層完成的。函式用 <code>**kwargs</code> 收到的關鍵字參數也開始照傳入順序排（PEP 468），底下靠的就是有序的 dict。</p>
<h2 id="37-變規格">3.7 變規格</h2>
<p>在 3.6，順序只是實作細節，把順序固定下來的則是 3.7：從 3.7 起，官方文件的說法改了，dict 的順序保證就是插入順序，也額外說了一句，順序從 3.6 開始本來只是 CPython 的實作細節，是刻意的觀察期，先確定新結構在真實程式裡正確，再寫進規格。</p>
<h2 id="更新和刪除之後">更新和刪除之後</h2>
<p>知道順序靠的是 entries 一路往後接，有兩個常見的行為就不奇怪了。更新已經存在的key，只改掉 entries 裡的值，位置沒動，順序上還是一樣。但先把某個key刪掉、再加回來，就不一樣了：刪除會在 indices 對應的格子標成特別狀態（原始碼裡叫 <code>DKIX_DUMMY</code>），在 entries 裡留成一個用不到的空位；等鍵重新加進來，會當成新的一筆接到最後。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">d</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;b&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">&#34;c&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">list</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>          <span class="c1"># [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">d</span><span class="p">[</span><span class="s2">&#34;a&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">99</span>      <span class="c1"># 更新既有</span>
</span></span><span class="line"><span class="cl"><span class="nb">list</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>          <span class="c1"># [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]，a 沒有換位置</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">del</span> <span class="n">d</span><span class="p">[</span><span class="s2">&#34;b&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">d</span><span class="p">[</span><span class="s2">&#34;b&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">5</span>       <span class="c1"># 先刪再加</span>
</span></span><span class="line"><span class="cl"><span class="nb">list</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>          <span class="c1"># [&#39;a&#39;, &#39;c&#39;, &#39;b&#39;]，b 跑到最後</span>
</span></span></code></pre></div><p>現在依賴 dict 的順序是安全的，只要記得更新鍵不會換位置，先刪再加的鍵會排到最後就好。</p>
<p>參考來源：<a href="https://docs.python.org/3/whatsnew/3.6.html#new-dict-implementation">What&rsquo;s New in Python 3.6</a>、<a href="https://docs.python.org/3/library/stdtypes.html#dict">內建型別文件的 dict 一節</a>，以及 CPython 的 <a href="https://github.com/python/cpython/blob/v3.13.7/Objects/dictobject.c"><code>Objects/dictobject.c</code></a>。</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
