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


421

カスタムクラスを作成する場合、==and !=演算子を使用して同等性を許可することがしばしば重要になります。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

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

とき__eq____ne__定義され、あなたは(私たちは後にしている一つである)、この動作を見つけることができます:

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

6
+1、私はdictが==にメンバーごとの等価性を使用していることを知らなかったため、同じオブジェクトdictに対して等しいものだけをカウントすると想定していました。PythonにはisオブジェクトIDと値の比較を区別する演算子があるので、これは明白だと思います。
SingleNegationElimination 2009

5
厳格な型チェックが実装されるように、受け入れられた回答は訂正またはAlgoriasの回答に再割り当てされると思います。
最大

1
また、ハッシュがオーバーライドされていることを確認してくださいstackoverflow.com/questions/1608842/...
アレックスPunnen

回答:


328

この単純な問題を考えてみましょう:

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クラシックスタイルクラスの==and !=演算子の非可換性の問題を修正するには、オペランドの型がサポートされていない場合に__eq__and __ne__メソッドがNotImplemented値を返す必要があります。ドキュメントには、定義NotImplementedとしての価値を:

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

この場合、演算子は比較演算を他のオペランドのリフレクトメソッドに委譲します。ドキュメント:定義のような方法を反映しました

これらのメソッドのスワップされた引数のバージョンはありません(左側の引数が操作をサポートしていないが、右側の引数はサポートしている場合に使用されます)。むしろ、__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

3
hash(tuple(sorted(self.__dict__.items())))の値の中にハッシュ化できないオブジェクトがある場合self.__dict__(つまり、オブジェクトのいずれかの属性がに設定されている場合など)は機能しませんlist
最大

3
そうですが、vars()にそのような変更可能なオブジェクトがある場合、2つのオブジェクトは実際には等しくありません...
Tal Weiss

12
すばらしい要約ですが、の代わりにを使用して実装__ne__==__eq__する必要があります。
Florian Brucker 2015年

1
三備考:1でのPython 3、実装する必要はありません__ne__もう、「デフォルトでは、__ne__()に委譲__eq__()結果と反転それがある場合を除きNotImplemented」。2.まだ実装したい場合__ne__は、より一般的な実装(Python 3で使用されているもの)は次のとおりx = self.__eq__(other); if x is NotImplemented: return x; else: return not xです。3.所与__eq__及び__ne__実装は準最適である:if isinstance(other, type(self)):22回の与える__eq__と10 __ne__ながら、通話をif isinstance(self, type(other)):16回の与える__eq__と6 __ne__コールを。
Maggyero

4
彼は優雅さについて尋ねましたが、彼は頑強になりました。
GregNash

201

継承には注意する必要があります。

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

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

次のように、タイプをより厳密にチェックします。

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

その上、あなたのアプローチはうまくいきます、それが特別な方法があるためです。


これは良い点です。組み込み型のサブクラス化によって、どちらの方向でも同等にできるため、同じ型であることを確認することは望ましくないこともあるので、注意する価値があると思います。
gotgenes 2009

12
タイプが異なる場合はNotImplementedを返し、比較をrhsに委任することをお勧めします。
最大

4
@maxの比較は、必ずしも左側(LHS)から右側(RHS)に行われる必要はなく、RHSからLHSに行われます。stackoverflow.com/a/12984987/38140を参照してください。それでも、NotImplemented提案どおりに戻ると常にが発生しますがsuperclass.__eq__(subclass)、これは望ましい動作です。
gotgenes 2013年

4
大量のメンバーがいて、オブジェクトコピーがあまり多くない場合は、通常、最初のIDテストを追加することをお勧めしますif other is self。これにより、より長いディクショナリの比較が回避され、オブジェクトがディクショナリキーとして使用される場合に大幅な節約になります。
デーンホワイト

2
そして、実装することを忘れないでください__hash__()
デーンホワイト

161

あなたが説明する方法は、私がいつもそれをしてきた方法です。完全に汎用的なため、いつでもその機能をミックスインクラスに分割し、その機能を必要とするクラスに継承することができます。

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

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

class Foo(CommonEqualityMixin):

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

6
+1:サブクラスでの簡単な置き換えを可能にする戦略パターン。
S.Lott、2008

3
isinstance sucks。なぜそれをチェックするのですか?なぜself .__ dict__ == other .__ dict__だけではないのですか?
nosklo 2008

3
@nosklo:わかりません..まったく関係のないクラスの2つのオブジェクトが偶然同じ属性を持つ場合はどうなりますか?
最大

1
noksloがインスタンスのスキップを提案していると思いました。その場合other、がのサブクラスかどうかはわかりませんself.__class__
最大

10
__dict__比較のもう1つの問題は、同等性の定義で考慮したくない属性がある場合(たとえば、一意のオブジェクトID、またはタイムスタンプのようなメタデータ)です。
アダムパーキン

14

直接的な回答ではありませんが、時々冗長な退屈な作業を省くため、十分に関連性があるように思われました。ドキュメントから直接カット...


functools.total_ordering(cls)

1つまたは複数の豊富な比較順序付けメソッドを定義するクラスが与えられると、このクラスデコレータが残りを提供します。これにより、可能な豊富な比較操作のすべてを指定するための作業が簡素化されます。

クラスは次のいずれかを定義しなければならない__lt__()__le__()__gt__()、または__ge__()。さらに、クラスは__eq__()メソッドを提供する必要があります。

バージョン2.7の新機能

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

1
ただし、total_orderingには微妙な落とし穴があります:regebro.wordpress.com/2010/12/13/…。注意してください!
Mr_and_Mrs_D 2016年

8

あなたは、両方をオーバーライドする必要はありません__eq__し、__ne__あなただけ上書きすることができます__cmp__が、これは==、!==、<、>などの結果に意義を行います。

isオブジェクトの同一性をテストします。これは、isa Trueとbの両方が同じオブジェクトへの参照を保持している場合にa bが存在することを意味します。Pythonでは常に、実際のオブジェクトではなく変数内のオブジェクトへの参照を保持するため、基本的にaがbである場合、それらのオブジェクトは同じメモリ位置に配置する必要があります。どのようにそして最も重要なのに、なぜあなたはこの振る舞いをオーバーライドしようとするのですか?

編集:私は__cmp__python 3から削除されたことを知りませんでしたので、それを避けてください。


なぜなら、オブジェクトの平等の定義が異なる場合があるからです。
Ed S.

is演算子を使用すると、オブジェクトIDに対するインタープリターの回答が得られますが、cmp
Vasil

7
Python 3では、「cmp()関数はなくなり、__ cmp __()特殊メソッドはサポートされなくなりました。」is.gd/aeGv
gotgenes 2008


2

私が探している2つの用語は、平等(==)と同一性(is)だと思います。例えば:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object

1
たぶん、2つのリストの最初の2つの項目のみを比較するクラスを作成でき、それらの項目が等しい場合はTrueと評価されます。これは同等だと思います。eqで完全に有効です。
gotgenes 2008

ただし、「is」はアイデンティティのテストであることに同意します。
gotgenes 2008

1

'is'テストは、本質的にオブジェクトのメモリアドレスを返す組み込みの 'id()'関数を使用してIDをテストします。そのため、オーバーロードできません。

ただし、クラスの等価性をテストする場合は、テストについてもう少し厳密にして、クラスのデータ属性のみを比較する必要があります。

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

このコードは、クラスの非関数データメンバーのみを比較し、一般的に必要なプライベートをスキップします。プレーンな古いPythonオブジェクトの場合、__ init __、__ str __、__ repr __、__ eq__を実装する基本クラスがあるので、POPOオブジェクトは余分な(ほとんどの場合同一の)ロジックの負担を負いません。


少し細かいですが、独自のis_()メンバー関数(2.3以降)を定義していない場合にのみ、id()を使用して「is」テストを行います。[ docs.python.org/library/operator.html]
2010年

「オーバーライド」とは、オペレーターモジュールをモンキーパッチングすることを意味していると思います。この場合、ステートメントは完全に正確ではありません。演算子モジュールは便宜上提供されており、これらのメソッドをオーバーライドしても「is」演算子の動作には影響しません。「is」を使用した比較では、常にオブジェクトのid()を使用して比較します。この動作はオーバーライドできません。また、is_メンバー関数は比較に影響を与えません。
mcrute

mcrute-私はあまりにも早く(そして間違って)話しました、あなたは完全に正しいです。
2010年

これは、特に__eq__が宣言されるときに非常に優れたソリューションですCommonEqualityMixin(他の回答を参照)。これは、SQLAlchemyでBaseから派生したクラスのインスタンスを比較するときに特に役立つことがわかりました。比較しないよう_sa_instance_stateに変更key.startswith("__")):しましたkey.startswith("_")):。また、それらにいくつかの後方参照があり、Algoriasからの回答によって無限の再帰が生成されました。その'_'ため、比較時にすべてスキップされるように、すべての後方参照に次の名前を付けました。注:Python 3.xではに変更iteritems()items()ます。
Wookie88 2013年

@mcrute通常、ユーザーが定義__dict____ない限り、インスタンスの先頭には何もありません。物事は好き__class____init__など、インスタンスのではありません__dict__が、むしろそのクラスで__dict__。OTOH、プライベート属性は簡単に始まり__、おそらくに使用する必要があります__eq____接頭辞付きの属性をスキップするときに、何を避けようとしていたのか正確に説明できますか?
最大

1

サブクラス化/ミックスインを使用する代わりに、ジェネリッククラスデコレーターを使用したい

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

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

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

使用法:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b

0

これにはAlgoriasの回答に関するコメントが組み込まれており、dict全体については気にしないので、オブジェクトを単一の属性で比較します。hasattr(other, "id")本当でなければならないが、私はそれをコンストラクタで設定したのでそれがわかっている。

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.