Python – 變數與資料型別
單元:id() 與物件位址概念
簡介
在 Python 中,一切皆是物件。無論是整數、字串、函式,甚至是模組本身,都以物件的形式存在於記憶體裡。了解物件在記憶體中的位置(即「位址」)對於除錯、效能分析以及避免不預期的行為非常重要。id() 內建函式正是提供這項資訊的關鍵工具:它回傳 物件的唯一識別碼,在 CPython 實作中這個識別碼即是物件的記憶體位址。掌握 id() 的用法,能幫助初學者看清變數與物件之間的關係,也能讓中級開發者在設計可變與不可變資料結構時避免常見的陷阱。
核心概念
1. 什麼是 id()?
id(object)會回傳一個整數,代表 該物件在執行期間的唯一身份。- 在 CPython(最常見的 Python 實作)中,
id()的值等同於物件在記憶體中的位址(memory address),但在其他實作(如 PyPy、Jython)則僅保證唯一性,不一定是實體位址。
註:
id()的返回值在物件生命週期內保持不變;當物件被垃圾回收後,該位址可能會被重新分配給新物件。
2. 變數與物件的關係
在 Python 中,變數其實是 指向物件的名稱(reference)。同一個物件可以被多個變數指向,這時這些變數的 id() 會相同;相反地,即使兩個變數的值相等(==),只要它們指向不同的物件,id() 就會不同。
3. 可變 vs. 不可變物件
- 不可變(immutable):如
int、float、str、tuple。一旦建立,內容不可改變。修改值時,Python 會 產生新物件,舊物件的id()與新物件的id()不同。 - 可變(mutable):如
list、dict、set。可以直接在原位修改,id()會保持不變。
4. 小整數快取(Integer Interning)
CPython 為了效能,會「快取」-5 到 256 之間的整數物件。這意味著這些數字在程式中多次出現時,會共用同一個物件(相同 id()),即使它們是由不同變數產生。
程式碼範例
以下範例皆使用 python 語法標記,並附上說明註解。
# 範例 1:基本的 id() 使用
a = 1000 # 大於 256 的整數不會被快取
b = 1000
print(id(a), id(b)) # 兩個物件的 id 通常不同
# => 140704123456784 140704123456800
c = 10 # 小於 256 的整數會被快取
d = 10
print(id(c), id(d)) # 兩個變數指向同一個物件
# => 140704123456720 140704123456720
# 範例 2:不可變物件的重新指派
x = "hello"
print(id(x)) # 假設為 140704123457000
x += " world" # 產生新字串物件
print(id(x)) # id 改變,說明字串是不可變的
# => 140704123457032
# 範例 3:可變物件的原位修改
lst = [1, 2, 3]
print(id(lst)) # 140704123458000
lst.append(4) # 原位修改,不會產生新物件
print(id(lst)) # id 保持相同
# => 140704123458000
# 範例 4:變數指向同一個可變物件
a = [10, 20]
b = a # b 只是另一個名稱,指向同一個 list
print(id(a), id(b)) # 兩者 id 完全相同
b.append(30) # 透過 b 修改,a 也會看到變化
print(a) # [10, 20, 30]
# 範例 5:使用 id() 判斷是否為同一物件
def is_same_object(o1, o2):
return id(o1) == id(o2)
print(is_same_object(5, 5)) # True,因為小整數快取
print(is_same_object([1], [1])) # False,兩個獨立的 list
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
誤以為 = 會「複製」物件 |
a = b 只會讓 a 指向 b 的物件,id(a) == id(b)。 |
若需要真正的副本,使用 copy.copy()(淺層複製)或 copy.deepcopy()(深層複製)。 |
| 忽略不可變物件的重新建立 | x = x + 1 其實是建立新整數,舊的 id() 失效。 |
在需要保留原始物件時,先儲存舊 id() 或使用可變容器(如 list)暫存。 |
| 依賴小整數快取的行為 | 只因 -5~256 內的整數會被快取,認為所有相等的數字都共享同一個物件。 |
不要以 id() 判斷「值相等」的正確性,應使用 ==。 |
在多執行緒環境下檢查 id() |
物件在不同執行緒中仍是同一個實體,但若被回收再分配,id() 可能會變。 |
盡量避免在生命週期不明確的情況下依賴 id() 作為唯一標識。 |
將 id() 作為持久化鍵 |
id() 僅在程式執行期間有效,程式結束後會失效。 |
若需永久唯一識別碼,請使用 uuid 模組或自行產生的鍵值。 |
最佳實踐
- 使用
==判斷相等,僅在確定需要檢查「是否同一個物件」時才使用id()(或is關鍵字)。 - 適時使用
copy,避免因變數共用同一可變物件而產生意外副作用。 - 觀察
id()變化,可作為除錯工具,快速判斷是否真的在產生新物件。
實際應用場景
除錯與記憶體分析
在大型程式中,某段資料意外被改寫往往是因為多個變數指向同一個可變物件。利用id()檢查變數間的關係,可快速定位問題根源。實作自訂緩存(Cache)
若要在函式層級緩存計算結果,常會以 參數的id()作為快取鍵,確保同一個物件不會被重複計算。_cache = {} def heavy_compute(obj): key = id(obj) if key in _cache: return _cache[key] # 假設這裡有耗時運算 result = sum(obj) # 只是一個範例 _cache[key] = result return result實作單例模式(Singleton)
單例模式要求全程只產生一個實例。可以透過id()檢查是否已經存在該類別的實例。class Singleton: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance a = Singleton() b = Singleton() print(id(a) == id(b)) # True跨模組傳遞大型資料
當多個模組需要共享同一個大型資料結構(如 NumPy 陣列)時,使用id()可以確認所有模組真的在操作同一塊記憶體,避免不必要的複製。
總結
id()提供 物件在執行期間唯一的識別碼,在 CPython 中即是記憶體位址。- 變數是 指向物件的名稱,同一物件的多個變數會擁有相同的
id()。 - 不可變物件在修改時會產生新物件,
id()會改變;可變物件則可在原位修改,id()保持不變。 - 小整數快取、可變容器的引用共享以及垃圾回收的重新分配,都是使用
id()時需要留意的細節。 - 正確運用
id()能協助除錯、設計快取、實作單例等實務需求,但切勿將其當作永久唯一鍵使用。
透過本篇文章的概念與範例,讀者應已能辨識變數與物件的真實關係,並在撰寫 Python 程式時,以更安全、更高效的方式管理資料。祝你在 Python 的旅程中,玩得開心、寫得更好!