1. ホーム
  2. python

[解決済み] Pythonのクラスで等価性(equality)をサポートするエレガントな方法

2022-03-20 23:42:59

質問

カスタムクラスを記述する際、しばしば ==!= 演算子を使用します。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__ s?

備考 : 少し明確にしておきます。 __eq____ne__ が未定義の場合、このような動作になります。

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

ということです。 a == b は、次のように評価されます。 False を実行するので、本当に a is b は、同一性のテスト(すなわち、"である。 a と同じオブジェクトです。 b ?quot;)となります。

いつ __eq____ne__ が定義されている場合、このような動作になります(これが私たちが求めている動作です)。

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

解決方法は?

この簡単な問題を考えてみましょう。

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 として宣言され class A(object) または class A(B): ここで B は新スタイルのクラスです。Python 3 では、新スタイルのクラスは以下のように宣言されたものだけです。 class 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 のクラシックスタイルのクラスでは __eq____ne__ メソッドは NotImplemented の値は、オペランド型がサポートされていない場合に使用されます。また ドキュメント が定義している NotImplemented の値として使用します。

数値メソッドやリッチな比較メソッドは、次のような場合にこの値を返すことがあります。 は、指定されたオペランドに対してその操作を実装していない。(その インタープリタは反映された操作を試すか、あるいは他の 演算子によってはフォールバックします)。真偽値はtrueである。

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

これらのメソッドには、引数を入れ替えたバージョン(Swapped-argument versions)はありません。 左の引数がその操作をサポートしていないが、右の引数がその操作をサポートしている場合 の引数があります)、むしろ __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

を返します。 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