Python クラスで等価性 (「同等性」) をサポートするエレガントな方法 質問する

Python クラスで等価性 (「同等性」) をサポートするエレガントな方法 質問する

==カスタム クラスを作成する場合、および演算子を使用して同等性を許可することが重要になることがよくあります。Python では、それぞれおよび特殊メソッド!=を実装することでこれが可能になります。これを行う最も簡単な方法は、次の方法です。__eq____ne__

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

これをよりエレガントに行う方法をご存知ですか? 上記の比較方法を使用することの特定の欠点をご存知ですか__dict__?

: 少し説明を加えます。 と が未定義の場合__eq____ne__次の動作が発生します。

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

a == bつまり、は と評価されます。これは、同一性のテスト (つまり、「 はと同じオブジェクトか?」)Falseを実際に実行するためです。a is bab

__eq__と が定義されている場合__ne__、次の動作が見られます (これが私たちが求めているものです)。

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

ベストアンサー1

次の簡単な問題を考えてみます。

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

したがって、Python はデフォルトで比較演算にオブジェクト識別子を使用します。

id(n1) # 140400634555856
id(n2) # 140400634555920

関数をオーバーライドすると__eq__問題は解決するようです:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Python 2では、__ne__関数をオーバーライドすることも忘れないでください。ドキュメンテーション状態:

比較演算子間には暗黙の関係はありません。 が真であることは、が偽であるx==yことを意味しませんx!=y。したがって、 を定義するときは__eq__()、演算子が期待どおりに動作するように も定義する必要があります__ne__()

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

Python 3では、これはもう必要ありません。ドキュメンテーション状態:

デフォルトでは、__ne__()は に委譲し__eq__()、 でない限り、結果を反転しますNotImplemented。比較演算子間には他の暗黙の関係はありません。たとえば、 が真であっても を(x<y or x==y)意味するわけではありませんx<=y

しかし、それではすべての問題が解決するわけではありません。サブクラスを追加してみましょう。

class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

注: Python 2 には 2 種類のクラスがあります。

  • クラシックスタイル(または旧スタイルの) クラスで、を継承せobject、 として宣言されているものclass A:class A():または が旧スタイルのクラスであるclass A(B):もの。B

  • 新しいスタイルクラスは を継承しobject、 または として宣言されています。ここではは新しいスタイルのクラスです。Python 3 には、、class A(object)またはとして宣言されている新しいスタイルのクラスのみがあります。class A(B):Bclass A:class A(object):class A(B):

クラシックスタイルのクラスの場合、比較演算は常に最初のオペランドのメソッドを呼び出しますが、新しいスタイルのクラスの場合、常にサブクラスのオペランドのメソッドを呼び出します。オペランドの順序に関係なく

ここでは、Numberクラシック スタイルのクラスは次のようになります。

  • n1 == n3通話n1.__eq__;
  • n3 == n1通話n3.__eq__;
  • n1 != n3通話n1.__ne__;
  • n3 != n1呼び出しますn3.__ne__

そして、Number新しいスタイルのクラスの場合:

  • 両方n1 == n3n3 == n1呼び出しn3.__eq__;
  • 両方n1 != n3n3 != n1呼び出しますn3.__ne__

Python 2のクラシックスタイルクラスの==and演算子の非可換性の問題を修正するために、オペランドの型がサポートされていない場合は、andメソッドは値を返す必要があります。!=__eq____ne__NotImplementedドキュメンテーション値を次のように定義しますNotImplemented

数値メソッドと豊富な比較メソッドは、提供されたオペランドに対する演算を実装していない場合、この値を返すことがあります。(インタープリターは、演算子に応じて、反映された演算またはその他のフォールバックを試行します。) その真理値は true です。

この場合、演算子は比較演算を他のオペランドの反映されたメソッドに委任します。ドキュメンテーション反映されたメソッドを次のように定義します。

これらのメソッドには、引数を入れ替えたバージョンはありません (左の引数が操作をサポートしていないが、右の引数がサポートしている場合に使用します)。むしろ、__lt__()と は__gt__()互いの反射であり、__le__()__ge__()互いの反射であり、__eq__()と は__ne__()自身の反射です。

結果は次のようになります。

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is NotImplemented:
        return NotImplemented
    return not x

オペランドが関連のない型 (継承なし) である場合にand演算子の可換性NotImplementedが求められる場合は、新しいスタイルのクラスでもの代わりに の値を返すFalseのが適切です。==!=

まだそこまで到達していませんか? まだです。固有の番号はいくつありますか?

len(set([n1, n2, n3])) # 3 -- oops

セットはオブジェクトのハッシュを使用し、デフォルトでは Python はオブジェクトの識別子のハッシュを返します。これをオーバーライドしてみましょう。

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

最終結果は次のようになります (検証のために最後にいくつかのアサーションを追加しました):

class Number:

    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2

おすすめ記事