Python 單元:運算子(Operators) – 身分運算子(is / is not)
簡介
在 Python 中,身分運算子 is 與 is not 用來判斷兩個變數 是否指向同一個物件。這與比較運算子 ==、!= 不同,後者比較的是「值」是否相等,而不是「記憶體位址」是否相同。
了解 is 的行為對於除錯、記憶體管理以及撰寫正確的條件判斷相當重要,尤其在處理 singleton(單例)物件(例如 None、True、False)或自訂類別時,常會用到身分比較。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,一步步帶你掌握 is / is not 的正確使用方式,讓你在實務開發中能夠寫出更安全、更易讀的程式碼。
核心概念
1. 什麼是「身分」比較?
- 身分(identity):指的是 物件在記憶體中的唯一位置(即
id()的返回值)。 a is b→ True 當且僅當id(a) == id(b)。a is not b→ True 當且僅當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 not 與 is 完全相反,常用於排除 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 來比較整數或字串的值,除非明確是 None、True、False 等單例。 |
忘記 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)或原子操作保證檢查與使用的原子性。 |
最佳實踐清單
- 僅在檢查單例(
None、True、False、自訂 Singleton)時使用is / is not。 - 比較值時使用
== / !=,並在需要時實作__eq__。 - 寫條件式時,保持
is not None的語法順序,避免not x is None產生歧義。 - 在除錯或日誌中,若想顯示物件身分,可使用
id(obj)。 - 對於可變容器,若想確認是否已被其他變數引用,才使用
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比較的是物件的身分(記憶體位址),而非值。- 最常見且安全的使用情境是 單例檢查(特別是
None、True、False)以及 同一個實例的辨識(如 Singleton、快取)。 - 千萬不要把
is當作一般的相等比較,尤其在可變容器或跨平台開發時,快取行為會導致不一致的結果。 - 使用
is not None時,請保持正確的語法順序,避免not x is None帶來的優先順序問題。 - 在需要比較「值」時,仍應使用
==,並視需求實作__eq__。
掌握身分運算子的正確概念與使用時機,能讓你的 Python 程式在 可讀性、效能與正確性 上都更上一層樓。祝你寫程式順利,持續探索 Python 的無限可能!