a = 256
b = 256
print(a is b) # 預期輸出: True
x = 257
y = 257
print(x is y) # 預期輸出: 可能是 True 或 FalseTrue
False
id() 函式Anthony
2026年4月14日
先來看一段會讓很多人愣住的程式碼:
True
False
同樣是「兩個變數被賦值為相同的整數字面量」,為什麼 256 這組會是 True,換成 257 之後卻可能變成 False?更弔詭的是,把這段程式放在 .py 檔裡執行,第二個結果常常又會變回 True;但若在 REPL 一行一行敲進去,則幾乎都是 False1。這種看似違反直覺的行為,其實背後都有一個合理的解釋——而解開這個謎團的鑰匙,正是本文的主角:id() 函式。
id() 語法概說根據 Python 官方文件,id(object) 回傳一個整數,該整數在該物件的生命週期 (lifetime) 內是唯一且不變的。換句話說,它滿足兩個性質:
id() 不會變。id()。在 CPython (也就是我們最常使用的 Python 實作版本) 中,這個整數通常就是物件在記憶體中的位址。
在正式進入進階議題之前,我們先透過一個簡單的範例,觀察 id() 的基本用法:
細心的讀者肯定有發現,id() 接受一個參數,回傳一個整數。這個整數本身沒有什麼意義,真正有意義的是「兩個 id() 是否相等」,因為這代表了兩個名稱是否指向同一個物件。
Python 甚至為這個概念提供了一個專屬的關鍵字 is,其本質上等價於比較兩個物件的 id():
前面提到 id() 具有「唯一且不變」的特性,但這句話有一個容易被忽略的前提:只有在該物件活著時才成立。當物件被回收後,舊的 id() 有可能被新物件重用2。
139646949225920
139646949226016
139646949225920
上述程式每次都建立一個臨時物件,印完就沒有任何名稱指向它,於是馬上可以被回收。CPython 可能很快重用同一塊記憶體,因此才會出現 id() 重複的現象。
若想安全地比較兩個物件是不是同一個,應該先把它們用變數留住:
回到一開始的 256 與 257 範例,或許已經可以猜到答案的輪廓。在 CPython 中,直譯器啟動時會預先建立一段常用整數物件,通常是 -5 到 256,放在所謂的小整數快取 (small int cache) 裡。當程式中寫出這個範圍內的整數,Python 會直接重用既有物件,不會再新建一個。
超出這個範圍,就不再保證:
相關的 _PyLong_SMALL_INTS 定義可以在 CPython 原始碼的 Objects/longobject.c 中找到。不過要注意的是,這個範圍屬於版本相關的實作細節,並非語言規範保證的行為3。
既然 257 不在小整數快取內,為什麼有時候 x is y 的結果還是 True?答案就在於常數折疊 (constant folding) 與同一個常數池 (code object) 的重用機制。
先看同一個 .py 檔中的範例:
再看在 REPL 分兩行輸入的情境:
原因其實是:.py 編譯時,編譯器可能把相同字面量放進同一個常數池,兩次載入都拿到同一個物件;而 REPL 每一次輸入通常是獨立的編譯單位,常數池不共享。可以用 dis 模組來親自驗證:
3 RESUME 0
4 LOAD_CONST 0 (257)
STORE_FAST 0 (a)
5 LOAD_CONST 0 (257)
STORE_FAST 1 (b)
6 LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (a, b)
IS_OP 0 (is)
RETURN_VALUE
兩次都是 LOAD_CONST 1,代表使用同一個常數槽位。這就是為什麼看似一樣的程式,在不同的執行脈絡下,is 的結果會有差異。
此處所看到的是編譯器與執行環境的策略,而不是 Python 語言規範所要求的行為。把它當作效能調校與除錯的知識即可,千萬不要當成語意保證,更不要拿來作為商業邏輯的判斷依據。
除了整數,字串也有類似的重用機制,稱為字串駐留 (string interning)。常見情況下,符合識別字規則 (identifier-like) 的短字串會被 Python 自動 intern。簡單來說,如果兩個字串一模一樣,Python 有機會只在記憶體中留一份,讓多個變數一起指向它,如此便可省下空間,比較起來也更快。
但帶有空格或特殊符號的字串通常不保證:
若想明確要求共用,可以使用 sys.intern():
True
實務上,若你有大量重複的小字串 (例如 CSV 欄位名稱、狀態碼),適度地 intern 可以減少記憶體占用與比較成本:
在掌握 id() 的基本語意之後,接下來要面對的是一系列看似違反直覺、卻又反覆出現在日常程式中的現象。為什麼同樣的字面量有時共用、有時不共用?為什麼 is 與 == 偶爾會給出不同答案?為什麼物件被回收後,新物件竟可能「繼承」舊的身份編號?這些問題的背後,牽涉到 CPython 的快取策略、編譯器的常數折疊,以及記憶體管理的底層細節。
is 與 ==在 Python 中,is 與 == 經常被初學者混用,但兩者其實有著本質上的差異:
| 運算子 | 比較對象 | 底層機制 |
|---|---|---|
is |
物件身份 (identity) | 本質上等價於比較 id() |
== |
物件的值 (value) | 呼叫物件的 __eq__ 魔術方法 |
最重要的實務準則是:判斷 None 請一律使用 is。
那麼,為什麼 is None 會比 == None 好呢?
None 是一個單例 (singleton),全程式中只會有一個 None 物件,用身份比較語意最精準。== 會呼叫對方的 __eq__,可能產生非預期的副作用,甚至回傳非布林值。id()很多初學者會誤以為 id() 只能用在整數、字串這類基本型別上。事實上,在 Python 裡一切皆為物件,函式、類別、甚至模組本身也都是貨真價實的物件,因此它們也都能被 id() 觀察:
139646907984256
959276176
139646949349056
特別值得一提的是「模組」這個物件。模組常被視為單例,這是因為 Python 的 import 機制會透過 sys.modules 來快取載入過的模組:
這個機制也是 Python 生態系大量使用「模組層級狀態」的基礎之一。
id() 當成永久的追蹤鍵理解 id() 與 intern,對於效能調校與除錯有直接的價值。例如在資料分析世界裡,pandas 的 category dtype,底層概念也是把重複值映射成共享字典與代碼,藉此減少重複儲存。
但一個最常見的誤用是:拿 id() 當作長期追蹤物件的鍵值。
這種寫法在長時間執行的程序中特別危險。更穩健的做法是使用 weakref:
weakref 追蹤的是物件關聯,而不是可能被重用的數字身份,語意上正確得多。
關於 id() 函式的特性,以下敘述何者正確?
id() 回傳的整數唯一且不變id() 回傳的整數在程式執行期間永不重複id() 一定等於物件在記憶體中的位址id() 只能用於整數與字串等基本型別正確答案是第一項。根據 Python 官方文件,id() 的保證是「在該物件的生命週期內唯一且不變」。當物件被回收後,舊的 id() 可能被新物件重用,因此第二項錯誤。第三項只是 CPython 的實作細節,並非語言規範;第四項則完全錯誤,函式、類別、模組等任何物件都可以使用 id()。
在 CPython 中執行以下程式碼,最有可能的輸出是什麼?
True TrueTrue 與 (True 或 False) 皆可能False FalseFalse True256 落在 CPython 的小整數快取範圍 (通常是 -5 到 256) 內,因此 a is b 必定為 True。而 1000 不在此範圍,但若放在同一個 .py 檔中,常數折疊可能讓 x is y 也變成 True;若在 REPL 分行輸入則常為 False。因此第二項「True 與 (True 或 False) 皆可能」最為精確。
判斷一個變數是否為 None 時,以下哪一種寫法最為推薦?
x == Nonex.equals(None)x is Noneid(x) == 0推薦使用 x is None。原因有二:其一,None 是全程式中唯一的單例物件,用身份比較 (is) 的語意最為精準;其二,== 會呼叫對方的 __eq__ 魔術方法,可能產生副作用或回傳非預期的結果。
請說明下列兩段程式在 .py 檔中執行時,為什麼 print(a is b) 的結果可能不同?
情境一的 "hello" 符合識別字規則 (identifier-like),CPython 會自動進行字串駐留 (string interning),讓 a 與 b 指向同一個物件,因此 a is b 常為 True。情境二的 "hello world" 因為包含空格,通常不會被自動 intern,a 與 b 可能指向不同物件,結果可能是 True 也可能是 False。若想強制共用,可使用 sys.intern() 明確要求。
在這一章中,我們從 id() 這個看似簡單的內建函式出發,揭開了 Python 變數行為的神秘面紗。以下是幾個關鍵筆記:
id() 的使用準則id() 可能被新物件重用。weakref,而非 id()。id() 觀察。is vs ==:is 比身份、== 比值;判斷 None 一律用 is。-5 到 256 的整數物件。.py 檔中同一字面量可能共用常數池,REPL 則不會。sys.intern() 明確要求。本文所討論的許多現象 (如小整數快取、常數折疊、字串駐留等) 都是 CPython 的實作細節,並非 Python 語言規範所保證的行為。理解這些細節可以寫出更省記憶體、更易除錯的程式碼,但千萬不要把它們當成商業邏輯的硬依賴。
若尚未親自試過,強烈建議現在就打開終端機,分別在 REPL 與 .py 檔執行一次。同樣的程式碼在兩種環境下得到不同結果,正是本文後續章節要解答的核心謎題。↩︎
這也是為什麼在多執行緒或長時間執行的程式中,直接儲存 id() 來追蹤物件會是一個危險的做法,後文會再詳細說明。↩︎
常見說法是 -5..256,但 CPython 的最新開發分支已經可以看到不同的常數設定。若對實作細節有興趣,可直接參考 CPython 原始碼。↩︎