閉じる

PythonのNoneや小さな数・文字列で混乱するisと==の違い

Pythonではis==がしばしば混同されます。

特にNoneの比較や、小さな整数や短い文字列でisがたまたまTrueになる挙動が混乱の元です。

本記事では、両者の違いを「同一性」と「等価性」の観点から丁寧に解説し、PEP 8に沿った安全な書き方や、実装依存の落とし穴を避けるための具体的なコツを示します。

isと==の違いは同一性と等価性

isは同一オブジェクトかを判定

isは2つの変数が「まったく同じオブジェクト(同じ参照先)」であるかどうかを判定します。

isは演算子であり、ユーザー定義のクラスで振る舞いを上書きできません。

内部的にはid()が同じかを調べるイメージです。

サンプルコード

Python
# is は「同一性(identity)」の比較
a = [1, 2, 3]
b = [1, 2, 3]
c = a  # c は a と同じリストオブジェクトを参照

print(a is b)  # 別々に作られたリストなので False
print(a is c)  # 同じオブジェクトを指すので True

# 参照先(オブジェクトID)を見てみる
print(id(a), id(b), id(c))
実行結果
False
True
140123456789456 140123456789888 140123456789456

==は値が等しいかを判定

==は「等価性(value equality)」の比較です。

組み込み型やクラスの__eq__メソッドが呼び出され、値が等しいかどうかが判定されます。

つまり、別オブジェクトであっても中身が同じならTrueになり得ます。

サンプルコード

Python
# == は「値の等価性」の比較
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # 要素が同じなので True
print(a is b)  # オブジェクトは別なので False
実行結果
True
False

迷わないための基本ルール

日常のコーディングでは、次の原則に沿えばまず迷いません。

  • 値が同じかを調べたいときは==を使います。
  • Noneのようなシングルトン(唯一のインスタンスが存在する特別な値)との比較にはis/is notを使います。

この違いは次の表に要点をまとめます。

演算子意味拡張可否典型用途
is同一性(同じオブジェクトか)上書き不可NoneTrueなどシングルトン判定、キャッシュ共有の確認
==等価性(値が等しいか)__eq__で上書き可能数値・文字列・コレクションの値比較、ドメインオブジェクトの等価性

Noneの比較はisを使う(PEP 8)

Noneに==を使わない理由

PEP 8では、Noneとの比較はis/is notを使うことが推奨されています。

Noneはシングルトンであり、同一性で判定するのが正確です。

また、==はユーザー定義の__eq__に委ねられるため、思わぬ振る舞いを引き起こす可能性があります。

サンプルコード

Python
# None 比較は is / is not を使う
from typing import Optional

def find_even_or_none(x: int) -> Optional[int]:
    # 偶数なら値を返し、奇数なら None を返す
    return x if x % 2 == 0 else None

result = find_even_or_none(3)

# 良い例
if result is None:
    print("値は見つかりませんでした")

# 悪い例 (PEP 8 に反する)
if result == None:  # noqa: E711 (サンプルとして記載)
    print("この比較方法は推奨されません")
実行結果
値は見つかりませんでした
この比較方法は推奨されません

is not Noneの安全な書き方

「値が存在するなら処理する」という場面ではis not Noneを使います。

0や空文字""は偽と判定されますが、Noneとは意味が異なるため明確に区別するのが安全です。

サンプルコード

Python
value = 0

# None との厳密な区別が必要なとき
if value is not None:
    print("値は None ではありません")  # 0 でもここに入る
実行結果
値は None ではありません

TrueやFalseとの比較で避ける書き方

if x == Trueif x == Falseのような比較は避けます。

真偽値の判定は「Truthiness(真値性)」に従ってif x:if not x:と書くのがPythonicです。

どうしてもTrueそのものを厳密に判定したい特別な理由がある場合のみx is Trueを検討します。

サンプルコード

Python
# Truthiness を使った書き方が推奨
flag = 1  # 1 は True に等価だが、True そのものではない

# 推奨
if flag:
    print("真と判定されました")

# 非推奨
if flag == True:  # noqa: E712
    print("== True は避けましょう")

# 厳密に True そのものかを見たい稀なケースのみ
flag2 = True
if flag2 is True:
    print("厳密に True 本体です")
実行結果
真と判定されました
== True は避けましょう
厳密に True 本体です

小さな数や短い文字列でisがTrueになる理由

整数のキャッシュは実装依存である

CPythonではよく使われる小さな整数(典型的には-5〜256)がキャッシュされます。

そのため、この範囲の整数は同一オブジェクトを共有しやすく、isTrueになることがあります。

しかし範囲や挙動は実装依存であり、バージョンや処理系で変わる可能性があるため、これに依存したコードを書いてはいけません。

サンプルコード

Python
# 小さな整数は同一オブジェクトを共有することがある
a = 256
b = 256
print(a is b)  # 多くの CPython で True になりやすい

# 大きめの整数は共有されないことが多い(実装依存)
x = int("257")  # 実行時に生成して定数畳み込みを避ける
y = int("257")
print(x == y)   # 値は等しい
print(x is y)   # 多くの CPython で False になりやすい
実行結果
True
True
False

文字列のインターニングと偶然の一致

短い文字列や識別子風の文字列は、Pythonが自動的にインターニング(同じオブジェクトを共有)することがあります。

また、同一モジュール内の同一リテラルはコンパイル時にまとめられる場合があります。

これも実装依存で、isTrueになるのは「たまたま」です。

値比較には必ず==を使います。

必要に応じてsys.internで明示的にインターンできます。

サンプルコード

Python
import sys

s1 = "python"
s2 = "python"
print(s1 is s2)  # True になりやすいが保証はない

t1 = "".join(["py", "thon"])  # 実行時に生成
t2 = "".join(["py", "thon"])
print(t1 == t2)  # 値は等しい
print(t1 is t2)  # 別オブジェクトになりやすい

u1 = sys.intern("data-key")
u2 = sys.intern("data-" + "key")  # インターンすれば同一化される
print(u1 is u2)  # True
実行結果
True
True
False
True

id()で参照先を確認する

デバッグ時に「同じオブジェクトか」を確かめたい場合はid()で参照先を確認できます。

ただし、id()値の具体的な数値に意味を持たせるべきではありません。

サンプルコード

Python
a = "abc"
b = "".join(["a", "b", "c"])

print(id(a), id(b))   # たいていは異なる
print(a == b, a is b) # 等価性は True、同一性は False
実行結果
140123456700000 140123456700512
True False

正しい使い分けとアンチパターン

値の比較は==、シングルトン(Noneなど)はis

日常の比較は==で十分です。

NoneTrueFalseEllipsis(...)などのシングルトンを判定するときにのみis/is notを使います。

これにより、実装依存のキャッシュやインターニングに影響されない堅牢なコードになります。

サンプルコード

Python
data = {"count": 0}

# 値の比較
if data["count"] == 0:
    print("カウントは 0 です")

maybe_none = None

# シングルトン判定
if maybe_none is None:
    print("None です")
実行結果
カウントは 0 です
None です

リストや文字列の比較にisを使わない

ミュータブル(可変)なリストや、イミュータブルな文字列であっても、isで比較してはいけません。

isは「同じオブジェクトか」を見るだけで、「同じ内容か」は判定しません。

内容の比較には常に==を使います。

サンプルコード

Python
# 文字列でのアンチパターン
s = "abc"
t = "".join(["a", "b", "c"])

print(s == t)  # 内容は同じ -> True
print(s is t)  # オブジェクトは別 -> False (is を使うべきでない)

# リストでも同様
a = [1, 2]
b = [1, 2]
print(a == b)  # True
print(a is b)  # False
実行結果
True
False
True
False

カスタムクラスの==実装(eq)での注意点

独自クラスで値の等価性を定義するなら__eq__を正しく実装します。

型が異なる場合はNotImplementedを返し、ハッシュ可能にしたい場合は__hash__との整合性にも注意します。

isは同一性のみを判定し、__eq__には影響しません。

良い実装例(dataclassを活用)

Python
from dataclasses import dataclass

@dataclass(frozen=True)  # 不変にしてハッシュと等価性の整合を取りやすく
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)  # 値が同じ -> True
print(p1 is p2)  # 別インスタンス -> False
実行結果
True
False

手動で__eq__を実装する場合

Python
class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __eq__(self, other):
        # 型が違う場合は NotImplemented を返すのが礼儀
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1

print(p1 == p2)  # True (値が等しい)
print(p1 is p2)  # False (同一ではない)
print(p1 is p3)  # True  (同一)
実行結果
True
False
True

なお、__eq__を上書きして可変にしたクラスを辞書キーや集合に入れる場合は__hash__の設計が必要です。

等価なオブジェクトは同じハッシュであるべきで、変更不能(frozen)にするのが安全です。

NumPyの配列のように、==が要素ごとの配列を返す型もあるため、ライブラリごとの等価性の定義にも注意します。

まとめ

is==の違いは、「同一性」と「等価性」という根本から理解すると迷いません。

isは同一オブジェクトかを判定し、特にNoneなどのシングルトンで使います。

==は値の等価性を判定し、通常の数値、文字列、コレクション、そしてカスタムクラスの比較に用います。

小さな整数のキャッシュや文字列のインターニングでisが偶然Trueになることはありますが、これは実装依存であり、設計の拠り所にしてはいけません。

PEP 8に従い、None比較はis、真偽判定はTruthiness、値比較は==という原則を守れば、初学者でも堅牢で読みやすいPythonicなコードを書けます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!