Python 變數與資料型別:is vs ==
簡介
在 Python 中,比較是程式邏輯的基礎。常見的比較運算子有 ==、!=、>、<,但新手往往會忽略另一個同樣重要的運算子——is。is 用來檢查物件身分(identity),而 == 則比較值的相等性(equality)。兩者看起來相似,卻在底層行為上截然不同。若在不適當的情境下混用,程式可能會產生難以偵測的錯誤,甚至影響效能。
本篇文章將深入解析 is 與 == 的差異、使用時機以及常見陷阱,幫助你在寫 Python 程式時作出正確的比較選擇。
核心概念
1. 什麼是「物件身分」 (is)
在 Python 中,每個變數其實都是指向一個物件的參照。is 直接比較兩個變數是否指向同一個記憶體位址(即同一個物件)。
a = [1, 2, 3]
b = a # b 與 a 指向同一個 list 物件
c = [1, 2, 3] # 雖然內容相同,但 c 是另一個獨立的 list
print(a is b) # True → 同一個物件
print(a is c) # False → 雖然值相等,但不是同一個物件
重點:
is只關心「是否為同一個實體」,不在乎內容是否相同。
2. 什麼是「值的相等」 (==)
== 會呼叫物件的 __eq__ 方法,根據物件的類別定義來判斷「值是否相等」。對於內建型別(如 int、str、list)已經有預設的相等判斷規則。
a = [1, 2, 3]
c = [1, 2, 3]
print(a == c) # True → 兩個 list 的內容相同
重點:
==只關心「值是否相同」,不在乎它們是否是同一個實體。
3. 為什麼會有兩種比較方式?
- 效能考量:檢查身分只需要比較兩個指標,速度極快。
- 語意清晰:有時候我們真的需要確認兩個變數是否指向同一個物件(例如檢查單例、快取),此時使用
is更能表達意圖。
4. 常見的「小技巧」:None 的比較
在 Python 中,檢查變數是否為 None 時,官方文件建議使用 is 而非 ==,因為 None 是唯一且不可變的單例。
result = get_data()
if result is None: # ✅ 正確寫法
print("沒有取得資料")
使用 == 雖然在大多數情況下也會得到相同結果,但若某個自訂類別覆寫了 __eq__,可能會產生意外行為。
程式碼範例
範例 1:字串快取與 is 的行為
Python 會對短字串(長度 ≤ 20)自動做「interning」快取,使得相同內容的字串可能指向同一個物件。
s1 = "hello"
s2 = "hello"
print(s1 is s2) # True ← 由於字串被快取,指向同一個物件
s3 = "hello world!" # 超過快取上限
s4 = "hello world!"
print(s3 is s4) # False ← 雖然內容相同,但是不同物件
小提醒:不要依賴
is來比較字串相等,除非你明確知道字串已被 intern。
範例 2:自訂類別的 __eq__ 與 is
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True ← 透過 __eq__ 比較值
print(p1 is p2) # False ← 兩個獨立的實例
範例 3:列表的淺層與深層拷貝
import copy
orig = [[1, 2], [3, 4]]
shallow = orig[:] # 淺層拷貝,內部子列表仍指向同一個物件
deep = copy.deepcopy(orig) # 深層拷貝,全部都是新物件
print(orig[0] is shallow[0]) # True ← 子列表是同一個
print(orig[0] is deep[0]) # False ← 完全獨立的拷貝
# 使用 == 比較內容
print(orig == shallow) # True
print(orig == deep) # True
範例 4:檢查單例模式(Singleton)
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Logger, cls).__new__(cls)
return cls._instance
log1 = Logger()
log2 = Logger()
print(log1 is log2) # True ← 確認兩者是同一個實例
範例 5:is 與布林值的比較
布林值在 Python 中也只有兩個唯一實例 True 與 False,因此使用 is 比較是安全且可讀性高的寫法。
flag = (5 > 3) # 結果為 True
if flag is True:
print("條件成立")
常見陷阱與最佳實踐
| 陷阱 | 說明 | 正確做法 |
|---|---|---|
把 is 當作一般相等比較 |
例如 if a is b: 期待比較值相等,卻因為物件不同而得到 False。 |
使用 == 進行值比較;只有在需要確認同一個物件時才使用 is。 |
比較字串或數值時使用 is |
受 CPython 快取機制影響,短字串或小整數可能暫時返回 True,造成誤判。 |
永遠使用 ==,除非明確知道要檢查 None 或單例。 |
自訂類別忘記實作 __eq__ |
== 會退回物件身分比較,導致結果不符合預期。 |
為需要值比較的類別實作 __eq__(必要時配合 __hash__)。 |
混用 is 與 == 於同一條件 |
會降低程式可讀性,讓維護者困惑。 | 依需求挑選單一運算子,保持條件式簡潔。 |
最佳實踐
檢查
None時使用isif result is None: …比較布林值時使用
isif flag is True: …值相等比較統一使用
==- 包含字串、數字、容器(list、dict、set)等。
- 若容器內部元素是自訂類別,確保該類別實作
__eq__。
需要辨識同一個實例時才使用
is- 單例模式、快取物件、共享資源的辨識。
寫測試時同時檢查
is與==- 以確保程式在邏輯與身分上皆符合預期。
實際應用場景
1. 快取(Cache)與記憶體管理
在大型系統中,常會把計算結果或資料庫查詢結果快取為物件。檢查快取命中時,使用 is 能快速判斷是否已取得同一個實例,避免不必要的深層比較。
def get_user(id):
cached = _user_cache.get(id)
if cached is not None: # 快速身分檢查
return cached
user = query_db(id)
_user_cache[id] = user
return user
2. 單例(Singleton)與全域資源
如前面的 Logger 範例,is 用來確保全域唯一性,避免產生多個寫檔實例導致檔案衝突。
3. 多執行緒/多程序共享物件
在需要判斷兩個執行緒是否持有同一個鎖(Lock)時,is 能直接比較物件身分,保證同步機制的正確性。
if thread_lock is main_thread_lock:
print("同一把鎖")
4. 資料驗證與清理
在資料清理流程中,常會把「缺失值」統一設定為 None。此時使用 is 能避免因自訂類別的 __eq__ 產生誤判。
def clean(value):
if value is None:
return ""
return str(value).strip()
總結
**is**比較的是 物件身分(是否指向同一個記憶體位址),適用於檢查None、單例、快取或其他需要確認「同一個實體」的情境。**==**比較的是 值的相等性,是大多數日常比較的首選,尤其是字串、數字、容器以及自訂類別(只要有實作__eq__)。- 常見陷阱包括把
is當作普通相等比較、在字串或小整數上使用is、忘記實作__eq__等。 - 最佳實踐是:檢查
None用is、值相等用==、需要辨識同一個實例時才用is,並在自訂類別中正確實作比較方法。
掌握 is 與 == 的差異,不僅能寫出更 正確、可讀 的程式碼,也能在效能與資源管理上取得微小卻重要的優勢。祝你在 Python 的變數與資料型別世界裡,玩得開心、寫得順手!