web-dev-qa-db-ja.com

Pythonクラスで等価性( "equality")をサポートするためのエレガントな方法

カスタムクラスを書くとき、==!=演算子を使って等価を許可することはしばしば重要です。 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は、a is bという同一性のテストを実際に実行するため、Falseと評価されます(つまり、「abと同じオブジェクトですか?」)。

__eq____ne__が定義されているとき、あなたはこの振る舞いを見つけるでしょう(これは私たちが後にしているものです):

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

この単純な問題を考えてください。

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では、 documentation のように、常に__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では、 documentation のようにこれはもう必要ありません。

デフォルトでは、__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種類のクラスがあります。

  • classic-style(またはold-style)クラスは、notobjectから継承し、class A:class A():として宣言されます。またはclass A(B):、ここでBはクラシックスタイルのクラスです。

  • objectから継承し、class A(object)またはclass A(B):として宣言されている---(new-styleクラス。ここでBは新しいスタイルのクラスです。 Python 3には、class A:class A(object):またはclass A(B):として宣言された新しいスタイルのクラスしかありません。

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

Numberがクラシックスタイルのクラスの場合

  • n1 == n3n1.__eq__を呼び出します。
  • n3 == n1n3.__eq__を呼び出します。
  • n1 != n3n1.__ne__を呼び出します。
  • n3 != n1n3.__ne__を呼び出します。

Numberが新しいスタイルのクラスの場合

  • n1 == n3n3 == n1はどちらもn3.__eq__を呼び出します。
  • n1 != n3n3 != n1はどちらもn3.__ne__を呼び出します。

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

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

この場合、演算子は比較操作をotherオペランドの反射されたメソッドに委任します。 documentation は反映されたメソッドを以下のように定義します。

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

==演算子と!=演算子のcommutativityが無関係の型である(継承がない)場合は、新しいスタイルのクラスに対しても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
261
Tal Weiss

継承には注意が必要です。

>>> 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

それ以外にも、あなたのアプローチはうまくいくでしょう、それが特別な方法があるのです。

191
Algorias

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

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
155
cdleary

直接的な答えではありませんが、時々冗長な作業を少し省くことができるので、取り組むには十分な意味があるようです。ドキュメントから直接切り取ります...


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()))
13
John Mee

__eq____ne__の両方をオーバーライドする必要はありません。__cmp__のみをオーバーライドすることができますが、これは==、!==、<、>などの結果に影響します。

isは、オブジェクトの同一性をテストします。つまり、aとbの両方が同じオブジェクトへの参照を保持している場合、a isbはTrueになります。 pythonでは、実際のオブジェクトではなく、常に変数へのオブジェクトへの参照を保持しているので、本質的にa is bが真になるためには、それらのオブジェクトは同じメモリ位置に配置されるべきです。どのようにそして最も重要なことに、なぜあなたはこの振る舞いを無効にしますか?

編集:私は__cmp__がpython 3から削除されたことを知らなかったので、それを避けてください。

8
Vasil

この答えから: https://stackoverflow.com/a/30676267/541136 私は、__ne____eq__という用語で定義するのは正しいのですが、それを証明しました。

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

あなたが使用する必要があります:

def __ne__(self, other):
    return not self == other
3
Aaron Hall

私はあなたが探している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
2
too much php

'is'テストは、本質的にオブジェクトのメモリアドレスを返す組み込みの '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オブジェクトはそれ以外の(そしてほとんどの場合は同一の)ロジックの負担を負いません。

1
mcrute

サブクラス化/ミックスインを使う代わりに、ジェネリッククラスデコレータを使うのが好きです。

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
bluenote10