「2つのオブジェクトが等しいか」を比べるとき、オブジェクトの「同一性(identity)」と「同値性(equivalence)」の2つの比較があります。
同一性と同値性(等価性)とは
ある参照が指しているオブジェクトと別の参照が指しているオブジェクトが「同一」であるというのは、2つの参照が同じオブジェクトを指している場合です。つまりオブジェクトの「同一性(identity)」とは、2つの参照が同じオブジェクトを指しているかどうか、言い換えれば2つの参照の値が同じかどうかを判定することです。
一方、2つのオブジェクトが同じ値(内容)であれば2つのオブジェクトは「同値」であるといいます。これは「同値性(equivalence)」の判定です。
実際にこれらの意味をコードで確認してみましょう。
>>> x = [1, 2, 3] >>> y = x >>> z = [1, 2, 3] >>> id(x) 4559016832 >>> id(y) 4559016832 >>> id(z) 4559016576 >>> x [1, 2, 3] >>> y [1, 2, 3] >>> z [1, 2, 3]
Pythonのオブジェクトは、オブジェクトごとにユニークな整数値(識別値)を持っています。組み込みのid()関数はこの識別値を返します。
つまり識別値が同じであればオブジェクトは同一です。一方、異なるオブジェクトでは必ず識別値が異なります。
これを踏まえて先程の出力結果を見てみましょう。変数xと変数yが指すオブジェクトの識別値は同じです。つまりこれらの参照はどちらも同じオブジェクを指しています。言い換えれば2つの参照が指すオブジェクトは「同一」です。
一方、変数zが参照するオブジェクトの識別値は変数xや変数yが指すオブジェクトの識別値とは異なります。これは変数zが指すオブジェクトと変数xや変数yが指すオブジェクトは「同一ではない」ということになります。
しかし、いずれの変数が指すオブジェクトも同じ値(内容)を持っています。これらのオブジェクトはどれもリストで要素として整数1、2、3を同じ順番で保持しています。
「同値性」に関していうと、変数x、変数y、変数zが指すオブジェクトはいずれも「同値」であるといえます。
同一性の判定には演算子isを使う
Pythonで同一性を判定するには演算子isを使います。isはid()関数を使って同一かどうかを判定します。つまり、さっきオブジェクトの識別値を目で見て比較していた方法と同じです。
>>> x is y True >>> x is z False >>> y is z False
結果は予想通りです。演算子isの被演算子が同じオブジェクトを参照していれば結果はTrueになります。
is notは反対の結果を返します。
>>> x is not z True
同値性の判定には演算子==を使う
次にオブジェクトの同値性をテストしてみましょう。これには演算子==を使います。演算子==はオブジェクトの値(内容)が同じならTrueを返します。
>>> x == y True >>> x == z True >>> y == z True
!=は反対の結果を返します。
>>> y != z False
同一性が等しければ、当然、同値性も等しいですが、同値性が等しいからといって同一性が等しいとは限りません。
オブジェクトが「同値」とは、どのようなときか?
同一性の判定方法は明確です。id()関数が返す識別値の同じなら、それらのオブジェクトは「同一」です。
では「オブジェクトが同値」とは、どのようなときでしょうか?
この答えはオブジェクトによって異なります。リストであれば2つのオブジェクトが同じ要素を同じ順番で保持していれば同値であると言えます。しかし、他の型のオブジェクトにこの定義は当てはまりません。
抽象的な言葉でこの問いに答えるとしたら「論理的に2つのオブジェクトが同じであると考えられる場合、2つのオブジェクトは同値あると言える」となるでしょう。
先程、演算子==を使えば2つのオブジェクトが同値か判定できる述べましたが、オブジェクトの判定方法が異なるのに、なぜいろいろなオブジェクトを演算子==で比較できるのでしょうか?
実は==の式が評価されると、被演算子であるオブジェクトの__eq__()関数が呼び出されます。そしてこの__eq__()関数が返す値が==の式の評価結果になります。
組み込みのオブジェクトでは同値のオブジェクトを比較すると__eq__()関数がTrueを返すようにあらかじめ実装されています。そのため==で同値の判定ができるわけです。
したがって==で同値を判定できるのは、正確にいうと「__eq__()関数が同値を判定できるように実装されたオブジェクト」ということになります。
自前のオブジェクトでこのことを確認してみましょう。例として次のように2次元空間の座標を表すPointクラスを定義します。
>>> class Point(): ... def __init__(self, x, y): ... self.x = x ... self.y = y ...
このオブジェクトはx座標とy座標を保持します。
__eq__()関数はPointクラスのスーパクラスであるobjectクラスから継承していますが、ここでは__eq__()関数をオーバーライドしていません。
実際にこのクラスのインスタンスを作成して同値性を判定してみましょう。
>>> p1 = Point(2, 3) >>> p2 = p1 >>> p3 = Point(2, 3) >>> p1 == p2 True >>> p1 == p3 False
p1とp3は全く同じ内容を持っていますが==の結果はFalseになります。これは__eq__()関数のデフォルトの実装が演算子isを使って判定しているからです。つまり__eq__()関数のデフォルトの実装では、2つの参照が同一の場合のみ2つのオブジェクトが同値であると判定します。p1とp2は同じオブジェクトを参照(つまりp1とp2のオブジェクト参照の値が同じ)なので当然Trueと判定されます。
この__eq__()関数のデフォルトの実装は多くのオブジェクトの場合は正しくありません。そこでPointクラスのオブジェクトを==で正しく比較できるように、__eq__()関数をオーバーライドしてみましょう。
>>> class Point(): ... def __init__(self, x, y): ... self.x = x ... self.y = y ... def __eq__(self, other): ... return (self.x == other.x) and (self.y == other.y) ... >>> p1 = Point(2, 3) >>> p2 = p1 >>> p3 = Point(2, 3) >>> p1 is p2 True >>> p1 is p3 False >>> p1 == p2 True >>> p1 == p3 True
__eq__()関数をオーバーライドしてPointオブジェクトの属性xと属性yが同じであればがTrueを返すようにしました。
実行結果を見ればわかる通り、これで「同一性」と「同値性」の判定が正しくできるようになりました。
このようにオブジェクトの同値性を正しく判定できるようにするのはクラスの実装者の責任です。
まぎらわしい「参照」という言葉
ちょっと横道にそれて、「参照」という言葉の意味について説明してみましょう。
前述のように名詞で「参照」という場合、「オブジェクト参照」のことを指します。この記事でも単に「参照」という言葉を使ってきました。しかし、この参照は「オブジェクトを参照する」というように動詞で使われることも多い言葉です。この場合は「オブジェクトを指し示す」あるいは「オブジェクトへアクセスする」というような意味に使われます。
このような意味の違いをきちんと理解していないと、ドキュメントなどの文章がいまいち理解できないことがあります。
名詞で使われる「参照」はオブジェクト指向言語の一般的な用語です。
一般にオブジェクト指向言語でオブジェクトを生成すると、そのオブジェクトを指し示す「参照」が返されます。次の式できちんと説明すると「’foo’という文字列オブジェクトが作成され、そのオブジェクトへの参照が返されます。そしてその参照が変数xへ代入されます。」というようになります。
x = 'foo'
しかし、このように厳密な言い方を毎回しないで、「文字列オブジェクト’foo’を変数xに代入する」といったり、「変数xは文字列オブジェクト’foo’を参照している」といったりしますので、よく理解してないと混乱することがあります。
実際の参照の値は何であるか?と疑問に思うかもしれません。しかしこれは言語の実装に依存します。わかりやす実装だどオブジェクトのメモリ上の番地そのもだったりします。つまりC言語のポインタのようなものです。
Pythonの参照の値については、残念ながらどのような値であるかを記述したドキュメントには出会えませんでした。実用上はオブジェごとにユニークな値を参照は持っていると考えれば良いでしょう。
参照を動詞として使うのは、オブジェクト指向の用語ということでは(たぶん)ありません。おそらく日本語へ翻訳すると「参照する」となったのだと思います。
まとめ
同一性と同値性の理解には、オブジェクト参照とオブジェクトの実体は別物であることがイメージできていれば、それほど難しい話ではありません。
同一性の比較は同値性の比較に比べて非常に高速です。同一性の比較(演算子is)が使えるときはこちらを使いましょう。