<?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>Data Model on KbWen Blog</title>
    <link>https://www.kbwen.com/tags/data-model/</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>Fri, 06 Nov 2026 14:30:00 +0800</lastBuildDate><atom:link href="https://www.kbwen.com/tags/data-model/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Python 寫了 __eq__ 之後，物件為什麼不能放進去 set</title>
      <link>https://www.kbwen.com/python-eq-disables-hash/</link>
      <pubDate>Fri, 06 Nov 2026 14:30:00 +0800</pubDate><dc:creator>KbWen</dc:creator>
      <guid>https://www.kbwen.com/python-eq-disables-hash/</guid>
      <description>替 class 寫了 __eq__，正常使用卻會出現 unhashable type 的錯誤。來看看 Python 為什麼這樣處理，以及 __hash__ 該怎麼做。</description>
      <content:encoded><![CDATA[<p>一個情境，如果想用 set 把重複的座標點去掉，class 裡寫好 <code>__eq__</code>，兩個座標相同的點用 <code>==</code> 比較也確實相等，但執行到建立 set 的時候就出現錯誤：</p>
<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">Point</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">Point</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">NotImplemented</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">y</span><span class="p">)</span> <span class="o">==</span> <span class="p">(</span><span class="n">other</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">other</span><span class="o">.</span><span class="n">y</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="n">a</span> <span class="o">=</span> <span class="n">Point</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">Point</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="o">==</span> <span class="n">b</span><span class="p">)</span>          <span class="c1"># True</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">Point</span><span class="o">.</span><span class="fm">__hash__</span><span class="p">)</span>  <span class="c1"># None</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">TypeError: unhashable type: &#39;Point&#39;
</span></span></code></pre></div><p>（從 Python 3.14 開始，這行訊息的開頭不同，會是 <code>TypeError: cannot use 'Point' as a set element (unhashable type: 'Point')</code>。說不定以後又會改了..）</p>
<p><code>Point.__hash__</code> 是 <code>None</code>，可是 class 裡並沒有設定過它。Python 的<a href="https://docs.python.org/3/reference/datamodel.html#object.__hash__">資料模型文件</a>覆寫了 <code>__eq__</code>、卻沒有定義 <code>__hash__</code> 的 class，<code>__hash__</code> 會被自動設成 <code>None</code>，之後只要取得這種實例的雜湊值，就會丟出 <code>TypeError</code> 錯誤。set 的元素和 dict 的 key 都需要雜湊值，所以錯誤發生在建立 set 的時候，前面的 <code>a == b</code> 反而沒事。</p>
<p>在沒有寫 <code>__eq__</code> 之前，<code>Point</code> 的實例其實可以放到 set。<a href="https://docs.python.org/3/glossary.html#term-hashable">術語表</a>自訂 class 的實例預設就是 hashable，只會跟自己相等，雜湊值則從 <code>id()</code> 來的。預設的 <code>==</code> 和雜湊值都看物件身分，兩邊是一致的（<code>is</code> 跟 <code>==</code> 的差別可以參考 <a href="/python-is-vs-equals/">Python 的 is 和 == 差在哪</a>）。寫了 <code>__eq__</code> 之後，<code>==</code> 改成比較座標，從 <code>id()</code> 來的雜湊值卻還是只看身分，會沒辦法對上，Python 也就不再沿用預設的雜湊值。</p>
<h2 id="用-id-補上">用 id 補上</h2>
<p>兩個方法裡各加一行 <code>print</code>，可以看到建立 set 時呼叫了哪些方法：</p>
<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">Point</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">y</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;呼叫 __eq__&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">Point</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">NotImplemented</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">y</span><span class="p">)</span> <span class="o">==</span> <span class="p">(</span><span class="n">other</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="n">other</span><span class="o">.</span><span class="n">y</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="fm">__hash__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;呼叫 __hash__&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">id</span><span class="p">(</span><span class="bp">self</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="n">a</span> <span class="o">=</span> <span class="n">Point</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">b</span> <span class="o">=</span> <span class="n">Point</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="o">==</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span> <span class="o">=</span> <span class="p">{</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">s</span><span class="p">))</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">呼叫 __eq__
</span></span><span class="line"><span class="cl">True
</span></span><span class="line"><span class="cl">呼叫 __hash__
</span></span><span class="line"><span class="cl">呼叫 __hash__
</span></span><span class="line"><span class="cl">2
</span></span></code></pre></div><p>錯誤沒有了，但原本想去掉的重複地方還在，set 裡有兩個元素。第一行的「呼叫 <strong>eq</strong>」來自 <code>print(a == b)</code>，建立 set 的時候只呼叫了兩次 <code>__hash__</code>，「呼叫 <strong>eq</strong>」反而沒有 print 出來。</p>
<p>這跟 set 加入元素的步驟有關。它先用 <code>__hash__</code> 的回傳值決定要到雜湊表的哪個位置找，如果碰到雜湊值相同的元素，才會再用 <code>==</code> 確認兩者是否相等。<code>a</code> 和 <code>b</code> 是兩個不同的物件，<code>id</code> 不同，雜湊值也就不同，set 不會再拿 <code>b</code> 跟 <code>a</code> 用 <code>==</code> 比較，所以 <code>__eq__</code> 當然就變得沒有用了。<a href="/python-dict-insertion-order/">dict</a> 的 key 也走同樣的流程，用 <code>Point(1, 2)</code> 當 key 存進去的值，換另一個 <code>Point(1, 2)</code> 也查詢不到。</p>
<p>也因為 set 先看雜湊值才比較，Python 對 <code>__hash__</code> 的硬性要求只有一個：相等的物件，雜湊值必須相同。用 <code>id</code> 補上的版本沒有做到這一點，執行時卻也沒有任何錯誤訊息。</p>
<h2 id="計算雜湊值">計算雜湊值</h2>
<p>把參與比較的欄位放進 tuple，同時計算這個 tuple 的雜湊值（資料模型文件也這樣建議）。<code>__eq__</code> 比較 <code>(self.x, self.y)</code>，<code>__hash__</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="k">def</span> <span class="fm">__hash__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;呼叫 __hash__&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">hash</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">y</span><span class="p">))</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">呼叫 __eq__
</span></span><span class="line"><span class="cl">True
</span></span><span class="line"><span class="cl">呼叫 __hash__
</span></span><span class="line"><span class="cl">呼叫 __hash__
</span></span><span class="line"><span class="cl">呼叫 __eq__
</span></span><span class="line"><span class="cl">1
</span></span></code></pre></div><p>這次的雜湊值相同，set 在同一個位置碰到 <code>a</code>，才呼叫 <code>__eq__</code> 確認，結果相等，<code>b</code> 沒有被加進去。座標相同的點不管建立幾次，計算出來的雜湊值都一樣，拿新的 <code>Point(1, 2)</code> 到 set 或 dict 裡查詢也沒問題。但是如果 <code>__hash__</code> 用到了 <code>__eq__</code> 不比較的欄位，就又會出現相等但雜湊值不同的情況。</p>
<h2 id="欄位被修改">欄位被修改</h2>
<p>用欄位計算雜湊值之後，還有一個地方要注意：<code>Point</code> 的 <code>x</code>、<code>y</code> 隨時都可以修改（下面的 <code>Point</code> 已經拿掉兩行 <code>print</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="n">a</span> <span class="o">=</span> <span class="n">Point</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">s</span> <span class="o">=</span> <span class="p">{</span><span class="n">a</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">a</span><span class="o">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span> <span class="ow">in</span> <span class="n">s</span><span class="p">)</span>           <span class="c1"># False</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">s</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">is</span> <span class="n">a</span><span class="p">)</span>  <span class="c1"># True</span>
</span></span></code></pre></div><p>set 裡唯一的元素就是 <code>a</code> 本身，但用 <code>a in s</code> 查詢卻得到 <code>False</code>。<code>a</code> 放進 set 時，位置是依照 <code>(1, 2)</code> 的雜湊值決定的；改成 <code>(10, 2)</code> 之後，查詢時是照新的雜湊值去找，原本的 <code>a</code> 找不到。</p>
<p>如果欄位在建立之後就不會再修改，像設定值這類資料，就用 <code>__eq__</code> 比較的同一組欄位計算 <code>__hash__</code>。如果欄位會在使用中被修改，<a href="https://docs.python.org/3/reference/datamodel.html#object.__hash__">資料模型文件</a>建議不要實作 <code>__hash__</code>，讓它維持 <code>None</code>；真的需要放進 set 或當成 dict key 時，可以改成把 <code>(p.x, p.y)</code> 這個 tuple 放進去，tuple 建立之後就不會變動。</p>
<p>這篇是照 Python 文件的資料模型與術語表整理的，範例則是在 Python 3.11 和 3.14 跑過。</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
