本文 AI 產出,尚未審核

Python 單元:運算子(Operators) – 身分運算子(is / is not


簡介

在 Python 中,身分運算子 isis not 用來判斷兩個變數 是否指向同一個物件。這與比較運算子 ==!= 不同,後者比較的是「值」是否相等,而不是「記憶體位址」是否相同。
了解 is 的行為對於除錯、記憶體管理以及撰寫正確的條件判斷相當重要,尤其在處理 singleton(單例)物件(例如 NoneTrueFalse)或自訂類別時,常會用到身分比較。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,一步步帶你掌握 is / is not 的正確使用方式,讓你在實務開發中能夠寫出更安全、更易讀的程式碼。


核心概念

1. 什麼是「身分」比較?

  • 身分(identity):指的是 物件在記憶體中的唯一位置(即 id() 的返回值)。
  • a is bTrue 當且僅當 id(a) == id(b)
  • a is not bTrue 當且僅當 id(a) != id(b)

範例

a = [1, 2, 3]
b = a          # b 直接指向 a 的同一個 list 物件
c = [1, 2, 3]  # 雖然內容相同,但建立了另一個獨立的 list 物件

print(a is b)   # True  (同一個物件)
print(a is c)   # False (不同物件)
print(a == c)   # True  (值相等)

2. 為什麼 is 常用於 None

None 是 Python 的唯一「空」物件,整個執行環境只會有 一個 None 實例。使用 is None 能保證判斷的是 是否真的沒有值,而非與其他等價的「偽」值(如空字串、0、空列表)混淆。

def foo(x):
    if x is None:          # 正確判斷
        print("沒有傳入參數")
    # if x == None:       # 雖然也會成立,但較不具可讀性且易誤用

3. is 與不可變物件的「快取」機制

對於小的整數、短字串與某些 bool,Python 會在啟動時建立 快取池(interning),使相同值的物件共享同一個身分。這會讓 is 在特定情況下看起來像是比較值,但這是實作細節,不應依賴

a = 256
b = 256
print(a is b)   # True (因為 CPython 內部快取了 0~256 的整數)

a = 1000
b = 1000
print(a is b)   # False (超出快取範圍,各自是不同物件)

結論:對於不可變物件,仍應使用 == 來比較值,除非你明確要檢查「是否同一個實例」。

4. is not 的語意

is notis 完全相反,常用於排除 None、避免同一個物件被重複處理等情境。

if result is not None:
    process(result)

程式碼範例

以下提供 5 個實用範例,說明 is / is not 在不同情境下的使用方式。

範例 1:判斷參數是否為 None(最常見的用法)

def connect(db_url: str | None):
    """
    若 db_url 為 None,使用預設的本機資料庫;否則使用指定的 URL。
    """
    if db_url is None:                # ✅ 正確的身分比較
        db_url = "sqlite:///local.db"
    print(f"Connecting to {db_url}")

connect(None)          # Connecting to sqlite:///local.db
connect("postgres://") # Connecting to postgres://

範例 2:避免在同一個物件上重複執行耗時操作

class DataCache:
    def __init__(self):
        self._data = None

    def load(self):
        if self._data is not None:          # 已經載入過,直接返回
            print("Cache hit")
            return self._data

        print("Loading data from source...")
        self._data = self._fetch_from_db()
        return self._data

    def _fetch_from_db(self):
        # 模擬耗時的 I/O
        return {"id": 1, "value": "sample"}

cache = DataCache()
cache.load()   # Loading data from source...
cache.load()   # Cache hit

範例 3:比較兩個自訂物件是否相同實例

class Node:
    def __init__(self, value):
        self.value = value

a = Node(10)
b = a                # b 與 a 指向同一個 Node
c = Node(10)         # 雖然值相同,但建立了新物件

print(a is b)        # True
print(a is c)        # False
print(a == c)        # False,除非自行實作 __eq__

小技巧:如果需要比較「結構相等」而非「同一個實例」,請實作 __eq__ 方法。

範例 4:使用 is not 篩選列表中的 None

items = [0, None, "", [], {}, None, "Python"]
# 移除所有 None,保留其他「偽」值
filtered = [x for x in items if x is not None]
print(filtered)   # [0, '', [], {}, 'Python']

範例 5:辨識單例(Singleton)模式的實作

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:          # 首次建立
            cls._instance = super().__new__(cls)
        return cls._instance

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)   # True,兩者指向同一個實例

常見陷阱與最佳實踐

陷阱 說明 解決方式
is 當作值相等比較 針對可變容器(list、dict)或大部份不可變物件使用 is 會得到意外的 False 使用 == 進行值比較;僅在需要判斷同一個實例時才用 is
依賴整數/字串快取 小整數或短字串在 CPython 中會被快取,導致 is 看似正確,但在其他實作或不同範圍會失效。 不要以 is 來比較整數或字串的值,除非明確是 NoneTrueFalse 等單例。
忘記 is not 的優先順序 if not x is None: 會被 Python 解析成 if (not x) is None:,結果錯誤。 使用 if x is not None:,或加上括號 if not (x is None):
自訂類別未實作 __eq__ 只靠 is 判斷會把結構相等的兩個實例視為不同。 為需要值相等比較的類別實作 __eq__(與 __hash__),保留 is 用於單例檢測。
在多執行緒環境下檢查 is None 後直接使用 可能因為競爭條件,另一執行緒已將變數改為非 None,導致錯誤。 使用鎖(threading.Lock)或原子操作保證檢查與使用的原子性。

最佳實踐清單

  1. 僅在檢查單例(NoneTrueFalse、自訂 Singleton)時使用 is / is not
  2. 比較值時使用 == / !=,並在需要時實作 __eq__
  3. 寫條件式時,保持 is not None 的語法順序,避免 not x is None 產生歧義。
  4. 在除錯或日誌中,若想顯示物件身分,可使用 id(obj)
  5. 對於可變容器,若想確認是否已被其他變數引用,才使用 is

實際應用場景

1. API 回傳值的「未提供」檢查

許多 Web 框架(如 Flask、FastAPI)在解析請求參數時,會以 None 表示「未提供」的欄位。使用 is None 能快速判斷是否需要套用預設值或拋出錯誤。

@app.post("/items")
def create_item(name: str | None = None):
    if name is None:
        raise HTTPException(status_code=400, detail="name is required")
    # 正常處理...

2. 快取與懶載入(Lazy Loading)

在大型系統中,物件的建立往往代價高昂。透過 is None 判斷是否已載入,可實作 懶載入 機制,提升效能。

class LargeReport:
    def __init__(self):
        self._data = None

    @property
    def data(self):
        if self._data is None:            # 首次存取才載入
            self._data = self._expensive_query()
        return self._data

3. 單例模式(Singleton)與全域設定

許多套件會以單例方式提供全域配置(例如 logging、資料庫連線池)。檢查 is 能保證全程只產生唯一實例。

logger = logging.getLogger("myapp")
if logger is logging.getLogger("myapp"):
    # 兩者指向同一個 Logger 實例

4. 事件循環與訊號處理

在非同步程式設計中,常會用 None 代表「尚未收到訊號」或「尚未排程」。透過 is not None 可安全觸發後續流程。

async def wait_for_message(queue):
    msg = await queue.get()
    if msg is not None:
        process(msg)

總結

  • is / is not 比較的是物件的身分(記憶體位址),而非值。
  • 最常見且安全的使用情境是 單例檢查(特別是 NoneTrueFalse)以及 同一個實例的辨識(如 Singleton、快取)。
  • 千萬不要把 is 當作一般的相等比較,尤其在可變容器或跨平台開發時,快取行為會導致不一致的結果。
  • 使用 is not None 時,請保持正確的語法順序,避免 not x is None 帶來的優先順序問題。
  • 在需要比較「值」時,仍應使用 ==,並視需求實作 __eq__

掌握身分運算子的正確概念與使用時機,能讓你的 Python 程式在 可讀性、效能與正確性 上都更上一層樓。祝你寫程式順利,持續探索 Python 的無限可能!