# 你是數字，我是數字，我們不一樣！

> 深入探討 Python 內部運作機制，解析 id() 函數在不同情境下的行為差異。本文透過 Bytecode 分析，詳細說明 Python 如何執行常數折疊（Constant Folding）與優化技術。了解為何 100+200 與變數相加的結果在記憶體位址上有所不同，並掌握 Python 3.12 針對數字效能的最佳化邏輯，是進階開發者不可錯過的技術指南。

Published: 2024-10-13
URL: https://kaochenlong.com/same-value-but-different-object-in-python

---

在 Python 裡的 `id()` 函數可以算出某個值或物件在 Python 世界裡的「編號」，如果有兩個值算出來的編號是一樣的，表示這兩個值不只相等，而且還是一顆物件。

那麼問題來了，我寫了一個程式叫做 `hello.py`，內容如下：

```python
a = 100
b = 200

def hey():
  return 300

res1 = 300
res2 = 100 + 200
res3 = a + b
res4 = hey()

print(id(res1))
print(id(res2))
print(id(res3))
print(id(res4))
```

當我執行 `python hello.py` 的時候，最後四行印出來的 id，有一個跟其它三個是不一樣的，猜猜看是哪一個。

&lt;!-- more --&gt;

答案是 `res3`，它算出來的數字跟其它三個不同。

先聲明，我使用的 Python 版本是 `3.12`，也許在不同的版本會有不同的結果。

為什麼這些看起來答案都是一樣的數值，卻有其中一個不一樣？在 Python 為了讓效能變的更好有做了不少調整，像是針對數字的效能最佳化，在 -5 ~ 256 之間的「小整數」，因為這些數字很常被使用，所以這些數值都直接編譯到 Python 直譯器本體，每次需要用到的時候就不用重新產生一個數字物件，就是因為這樣才會有以下這個看起來有點奇妙的現象：

```
&gt;&gt;&gt; a = 256
&gt;&gt;&gt; b = 256
&gt;&gt;&gt; a is b
True

&gt;&gt;&gt; a = 257
&gt;&gt;&gt; b = 257
&gt;&gt;&gt; a is b 
False
```

這應該也不是什麼秘密了，如果想知道更多細節，可參考[整數的前世今生](https://pythonbook.cc/chapters/cpython/numbers#%E5%B0%8F%E6%95%B4%E6%95%B8)文章介紹。

不過這題我有刻意避開了小整數的範圍，因為重點不在這裡。如果想要知道 Python 是怎麼執行我們寫的程式，就得看看 `hello.py` 編譯成 Bytecode 變成什麼樣子。因為內容有點長而且混在一起，所以我一段一段整理給大家看看。首先是 `res1 = 300` 這行：

```plaintext
7          16 LOAD_CONST               3 (300)
           18 STORE_NAME               3 (res1)
```

這相當簡單，就是載入一個常數 300，然後指定給 `res1` 變數。再看看 `res2 = 100 + 200`：

```plaintext
8          20 LOAD_CONST               3 (300)
           22 STORE_NAME               4 (res2)
```

咦？有發現奇怪的地方嗎？原本我們的程式碼是寫 `100 + 200`，但 Python 直接幫我們算好，然後把結果指定給 `res2`，看起來跟 `res1` 是一樣的。

Python 為了讓效能更好一點，在編譯成 Bytecode 之前會先進行「常數折疊」(Constant Folding) 的操作，試著計算那些可以提前得知結果的運算。例如這裡的 `100 + 200` 被先計算成 `300`，然後直接作為常數編譯到 Bytecode 中，這樣可以減少在執行階段的不必要計算量。

這裡還有個小細節，在 `LOAD_CONST 3` 後面的 `3`，這跟剛才 `res1 = 300` 的 `LOAD_CONST 3` 是一樣的，表示這是從同一格堆疊上取得並載入的值，所以這會是同一個常數 300。

我們暫時跳過第三個，先看最後一個 `res4 = hey()`：

```plaintext
 4          10 LOAD_CONST               2 (&lt;code object hey&gt;)
            12 MAKE_FUNCTION            0
            14 STORE_NAME               2 (hey)

10          34 PUSH_NULL
            36 LOAD_NAME                2 (hey)
            38 CALL                     0
            46 STORE_NAME               6 (res4)

Disassembly of &lt;code object hey&gt;:
 4           0 RESUME                   0

 5           2 RETURN_CONST             1 (300)
```

上半段是定義 `hey()` 函數，中段的 `CALL` 也很好猜，就是在呼叫 `hey()` 這個函數。最後一段的 `RETURN_CONST` 回傳一個常數 300。但是，這裡的 300 跟前面的 300 是同一個數字物件嗎？這裡就是細節了...

Python 在建立函數的時候，更精準的說是在建立函數裡面的 Code Object 的時候，裡面有個叫做 `const_cache` 的東西，這東西從名字看起來大概可以猜到是常數的暫存。簡單的說，就是在函數裡需要用到常數的時候，會先檢查是不是已經有人用過同樣的常數，如果有，就會直接取用同一個值。對這部份實作有興趣的話，可搜尋原始碼 `makecode()` 函數的實作。

好，到這裡，`res1`、`res2` 跟 `res4` 都是同一個常數 300，所以使用 `id()` 函數計算出來的結果都是一樣的。

那 `res3 = a + b` 呢？來看看它的操作：

```plaintext
1           2 LOAD_CONST               0 (100)
            4 STORE_NAME               0 (a)

2           6 LOAD_CONST               1 (200)
            8 STORE_NAME               1 (b)

9          24 LOAD_NAME                0 (a)
           26 LOAD_NAME                1 (b)
           28 BINARY_OP                0 (+)
           32 STORE_NAME               5 (res3)
```

這個行為明顯跟 `res1` 或 `res2` 不一樣了，這個 `BINARY_OP` 會拿前面堆疊上的 `a` 跟 `b` 以及加號 `+` 進行運算。如果再深入追原始碼的話（有興趣可搜尋原始碼的 `_PyLong_Add()` 函數），會發現 `a + b` 的過程會產生並回傳一顆新的數字物件，雖然它也是數字 300，但這個 300 跟前面用 `LOAD_CONST` 載入的 300 就不是同一個物件了。

當然，如果把這裡的數字改成 -5 ~ 256 之間的小整數的話，就會發現這四行的答案都是一樣的了。

另外，不知道大家有沒發現我刻意而且還有強調把這些程式碼寫在一個 `.py` 檔裡然後執行它，雖然 300 這個數字並不在小整數的範圍內，所以沒辦法享受當伸手牌拿現成數字來用的待遇，但因為執行整個檔案的時候，這個 300 就是一個被定義過的常數，在同一次的編譯裡可以被重複使用。

不過，如果你進到 Python 的 REPL 環境就不一樣了：

```
&gt;&gt;&gt; a = 100
&gt;&gt;&gt; b = 200
&gt;&gt;&gt; def hey():
...   return 300
...
&gt;&gt;&gt; res1 = 300
&gt;&gt;&gt; res2 = 100 + 200
&gt;&gt;&gt; res3 = a + b
&gt;&gt;&gt; res4 = hey()

&gt;&gt;&gt; id(res1)
4298043664
&gt;&gt;&gt; id(res2)
4296275504
&gt;&gt;&gt; id(res3)
4298043920
&gt;&gt;&gt; id(res4)
4296275632
```

在 REPL 不會有「常數折疊」的機制，而且每次的執行，都是一個全新的開始，每次的執行都會進行一次編譯，所以除非是 -5 ~ 256 之間的小整數，否則這四個 300 可能都會是不同的 300。


