is vs == の判断基準と CPython の整数キャッシュ(Python)

Python では ==is はまったく異なる比較を行う。== は値の等価性を、is はオブジェクトの同一性を比較する。この違いを正しく理解しないと、特に CPython の整数キャッシュなどの最適化により、予期せぬバグに遭遇することがある。

基本的な違い

==is の違いを確認しよう。

a = [1, 2, 3]
b = [1, 2, 3]
c = a

# == は値の等価性を比較
print(a == b)  # True(内容が同じ)
print(a == c)  # True

# is はオブジェクトの同一性(アイデンティティ)を比較
print(a is b)  # False(別のオブジェクト)
print(a is c)  # True(同じオブジェクト)

# id() で確認
print(id(a), id(b), id(c))
# 例: 140234567890 140234567891 140234567890

is は内部的に id() の値を比較している。同じメモリアドレスを指しているかどうかを確認するわけだ。

None との比較は is を使う

None は Python でシングルトン(唯一のインスタンス)として実装されている。そのため、None との比較には is を使うのが推奨される。

value = None

# 推奨
if value is None:
    print("value is None")

if value is not None:
    print("value is not None")

# 非推奨(動作するが)
if value == None:
    print("value equals None")

== でも動作するが、__eq__ がオーバーライドされたクラスでは予期せぬ結果になる可能性がある。

class Weird:
    def __eq__(self, other):
        return True  # 何と比較しても True

w = Weird()
print(w == None)   # True(意図しない結果)
print(w is None)   # False(正しい結果)

CPython の整数キャッシュ

CPython は、-5 から 256 までの整数をあらかじめキャッシュしている。この範囲の整数は、同じ値なら同じオブジェクトを参照する。

# キャッシュ範囲内(-5 〜 256)
a = 256
b = 256
print(a is b)  # True(同じオブジェクト)

# キャッシュ範囲外
a = 257
b = 257
print(a is b)  # False(異なるオブジェクト)

# 負の数も同様
a = -5
b = -5
print(a is b)  # True

a = -6
b = -6
print(a is b)  # False

この挙動は実装の詳細であり、仕様ではない。他の Python 実装(PyPy など)では異なる場合がある。

文字列のインターニング

CPython は特定の文字列も最適化のためにインターン(再利用)する。

# 短い文字列や識別子的な文字列はインターンされる
a = "hello"
b = "hello"
print(a is b)  # True(多くの場合)

# 連結で作成した文字列
c = "hel" + "lo"
print(a is c)  # True(コンパイル時に最適化される)

# 実行時に作成した文字列
d = "".join(["h", "e", "l", "l", "o"])
print(a is d)  # False(実行時に作成されるため)

# 文字列の値としては等しい
print(a == d)  # True

スペースや特殊文字を含む文字列は通常インターンされない。

a = "hello world"
b = "hello world"
print(a is b)  # False(スペースを含むため)
print(a == b)  # True

a = "hello!"
b = "hello!"
print(a is b)  # False(! を含むため)
print(a == b)  # True

なぜ is を値の比較に使ってはいけないのか

整数キャッシュや文字列インターニングの挙動に依存すると、以下のような問題が起きる。

def is_special_number(n):
    # 悪い例:is で比較
    return n is 256

print(is_special_number(256))  # True
print(is_special_number(256 + 0))  # True(キャッシュ範囲内)

def is_large_number(n):
    return n is 1000

print(is_large_number(1000))  # 結果は実装依存!
# 同じソースファイル内で定数として定義されていれば True になることも
# さらに危険な例
x = 257
y = 257

# 同じ行で定義すると最適化される場合がある
a = 300; b = 300
print(a is b)  # True になることも(同じコードブロック内の最適化)

# 対話モード(REPL)では異なる結果になることも

正しい使い分け

== を使うべき場合

値の等価性を比較したいとき。数値、文字列、リスト、辞書などの内容を比較する場合。

is を使うべき場合

オブジェクトの同一性を確認したいとき。NoneTrueFalse との比較。シングルトンパターンの実装。

# 正しい使い分け
value = get_something()

# None チェック:is を使う
if value is None:
    handle_none()

# 値のチェック:== を使う
if value == 0:
    handle_zero()

# ブール値のチェック:is を使うことが推奨される場合もある
if value is True:  # 厳密に True かどうか
    pass

if value:  # Truthy かどうか(これが一般的)
    pass

真偽値と is

TrueFalse もシングルトンだが、is での比較は注意が必要だ。

# True/False は is で比較できる
print(True is True)   # True
print(False is False) # True

# しかし、bool 以外の Truthy/Falsy 値との比較は危険
print(1 == True)   # True(int と bool の値比較)
print(1 is True)   # False(異なるオブジェクト)

print(0 == False)  # True
print(0 is False)  # False

通常は if value:if not value: のように、暗黙の真偽値変換を使う方がよい。

まとめ

==is の使い分けは明確だ。

基本原則

値を比較するなら ==、同一オブジェクトかどうかを確認するなら is。CPython の整数キャッシュや文字列インターニングに依存してはいけない。

実践的なルール

None との比較は is を使う。数値や文字列の比較は == を使う。シングルトンパターン以外では is による値比較を避ける。

この使い分けを守れば、実装依存のバグを避けられる。