本文 AI 產出,尚未審核

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__ 方法,根據物件的類別定義來判斷「值是否相等」。對於內建型別(如 intstrlist)已經有預設的相等判斷規則。

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 中也只有兩個唯一實例 TrueFalse,因此使用 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== 於同一條件 會降低程式可讀性,讓維護者困惑。 依需求挑選單一運算子,保持條件式簡潔。

最佳實踐

  1. 檢查 None 時使用 is

    if result is None: …
    
  2. 比較布林值時使用 is

    if flag is True: …
    
  3. 值相等比較統一使用 ==

    • 包含字串、數字、容器(list、dict、set)等。
    • 若容器內部元素是自訂類別,確保該類別實作 __eq__
  4. 需要辨識同一個實例時才使用 is

    • 單例模式、快取物件、共享資源的辨識。
  5. 寫測試時同時檢查 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__ 等。
  • 最佳實踐是:檢查 Noneis、值相等用 ==、需要辨識同一個實例時才用 is,並在自訂類別中正確實作比較方法。

掌握 is== 的差異,不僅能寫出更 正確、可讀 的程式碼,也能在效能與資源管理上取得微小卻重要的優勢。祝你在 Python 的變數與資料型別世界裡,玩得開心、寫得順手!