Python 클래스에서 동등성 (“평등”)을 지원하는 우아한 방법


421

사용자 정의 클래스를 작성할 때 ==!=연산자 를 사용하여 동등성을 허용하는 것이 종종 중요합니다 . 파이썬에서는 각각 __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 비교 방법을 사용하면 특별한 단점이 있습니까?

참고 : A는 설명의 비트 - 때 __eq____ne__정의되어 있지 않습니다이 동작을 확인할 수있는 것들 :

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

즉, 실제로 실행되는지 , 즉 신원 테스트 (즉, "? 와 동일한 개체 ") 이기 때문에 a == b평가됩니다 .Falsea is bab

__eq____ne__정의 되면 이 동작 (이후의 동작)을 찾을 수 있습니다.

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

6
+1, dict이 ==에 대해 구성원 별 평등을 사용한다는 것을 몰랐기 때문에 동일한 객체 dict에 대해서만 동일하게 계산한다고 가정했습니다. 파이썬에는 is객체 ID와 값 비교를 구별 하는 연산자 가 있기 때문에 이것이 분명하다고 생각합니다 .
SingleNegationElimination

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

에서 파이썬이 항상 우선 기억 __ne__은 AS뿐만 아니라, 기능을 문서 상태 :

비교 연산자 사이에는 내재 된 관계가 없습니다. 진실은 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

파이썬 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에는 두 가지 종류의 클래스가 있습니다.

  • 고전적인 스타일 (또는 이전 스타일 않음) 클래스 하지 상속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 !=연산자 의 비전 역 문제를 해결하려면 피연산자 유형이 지원되지 않을 때 __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

피연산자가 관련이없는 유형 (상속 없음) 인 경우 및 연산자의 정류 가 필요한 경우 새 스타일 클래스에 대해서도 NotImplemented값을 반환하는 False것이 옳은 일 입니다.==!=

우리는 아직있다? 좀 빠지는. 고유 번호는 몇 개입니까?

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

세트는 객체의 해시를 사용하며 기본적으로 파이썬은 객체 식별자의 해시를 반환합니다. 재정의 해 봅시다 :

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__있으면 (즉, 객체의 속성 중 하나가 a로 설정된 경우 list) 작동하지 않습니다.
최대

3
사실, 그러나 vars ()에 그러한 변경 가능한 객체가 있다면 두 객체는 ​​실제로 동일하지 않습니다 ...
Tal Weiss


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

12
유형이 다른 경우 NotImplemented를 반환하여 rhs에 대한 비교를 위임하는 것이 좋습니다.
최대

4
@max 비교는 반드시 왼쪽 (LHS)에서 오른쪽 (RHS)으로, RHS에서 LHS로 수행 할 필요는 없습니다. stackoverflow.com/a/12984987/38140을 참조하십시오 . 그래도 NotImplemented제안대로 반환 하면 항상 superclass.__eq__(subclass)바람직한 동작 인 cause 가 발생합니다.
gotgenes

4
많은 멤버가 있고 많은 객체 사본이 없다면 초기에 신원 테스트를 추가하는 것이 좋습니다 if other is self. 이것은 더 긴 사전 비교를 피하고 객체가 사전 키로 사용될 때 크게 절약 할 수 있습니다.
데인 화이트

2
구현하는 것을 잊지 마십시오__hash__()
Dane White

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

3
isinstance 짜증. 왜 확인해야합니까? 왜 self .__ dict__ == other .__ dict__ 아닌가?
nosklo

3
@nosklo : 이해가 안됩니다. 완전히 관련이없는 클래스의 두 객체가 동일한 속성을 가지면 어떻게됩니까?
최대

1
나는 nokslo가 인스턴스 건너 뛰기를 제안한다고 생각했다. 이 경우 더 이상의 other하위 클래스 인지 알 수 없습니다 self.__class__.
최대

10
__dict__비교의 또 다른 문제 는 평등의 정의에서 고려하지 않으려는 속성 (예 : 고유 한 개체 ID 또는 시간 생성 스탬프와 같은 메타 데이터)이있는 경우에 발생합니다.
Adam Parkin

14

직접적인 대답은 아니지만 때로는 약간의 장황한 테디 엄을 절약하기 때문에 압도적 인 것으로 보입니다. 문서에서 바로 자르십시오 ...


functools.total_ordering (cls)

하나 이상의 풍부한 비교 순서 방법을 정의하는 클래스가 주어지면이 클래스 데코레이터가 나머지를 제공합니다. 이렇게하면 가능한 모든 풍부한 비교 연산을 지정하는 데 드는 노력이 단순화됩니다.

클래스 중 하나 정의해야합니다 __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 12

8

당신은 모두를 오버라이드 (override) 할 필요는 없습니다 __eq__그리고 __ne__당신은 단지 오버라이드 할 수 __cmp__있지만, 이것은 ==,! ==, <,> 등의 결과에 시사점을 만들 것입니다.

is객체 식별 테스트 이는 a와 b가 모두 동일한 객체에 대한 참조를 보유하는 경우 isa가 b 임을 의미 True합니다. 파이썬에서는 항상 실제 객체가 아닌 변수의 객체에 대한 참조를 유지하므로 본질적으로 a가 b가되는 경우 객체의 객체는 동일한 메모리 위치에 있어야합니다. 이 행동을 어떻게 무시할 것인가?

편집 : __cmp__파이썬 3에서 제거되었다는 것을 몰랐 으므로 피하십시오.


때때로 당신은 당신의 객체에 대해 다른 평등의 정의를 가지고 있기 때문입니다.
Ed S.

is 연산자는 인터프리터에게 객체 식별에 대한 답변을 제공하지만 cmp
Vasil

7
파이썬 3에서 "cmp () 함수가 사라졌고 __cmp __ () 특수 메소드는 더 이상 지원되지 않습니다." is.gd/aeGv
gotgenes

4

이 답변에서 : 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

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
어쩌면 두 목록의 처음 두 항목 만 비교하는 클래스를 만들 수 있다는 점을 제외하고 해당 항목이 같은 경우 True로 평가 될 수 있습니다. 이것은 평등이 아니라 동등한 것입니다. 여전히 eq 에서 완벽하게 유효합니다 .
gotgenes

그러나 "is"는 정체성의 시험이라는 데 동의합니다.
gotgenes

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

이 코드는 클래스의 함수가 아닌 데이터 멤버를 비교하고 일반적으로 원하는 개인 정보는 건너 뜁니다. Plain Old Python Objects의 경우 __init__, __str__, __repr__ 및 __eq__을 구현하는 기본 클래스가 있으므로 POPO 객체가 추가 (및 대부분의 경우 동일한) 논리의 부담을 갖지 않습니다.


비트 nitpicky이지만 'is'는 자체 is_ () 멤버 함수 (2.3+)를 정의하지 않은 경우에만 id ()를 사용하여 테스트합니다. [ docs.python.org/library/operator.html]
02 초

"재정의"를하면 실제로 운영자 모듈을 원숭이 패치하는 것을 의미합니다. 이 경우 귀하의 진술이 완전히 정확하지는 않습니다. 연산자 모듈은 편의를 위해 제공되며 이러한 메소드를 대체해도 "is"연산자의 동작에 영향을 미치지 않습니다. "is"를 사용하는 비교는 항상 비교를 위해 객체의 id ()를 사용하므로이 동작을 재정의 할 수 없습니다. 또한 is_ 멤버 함수는 비교에 영향을 미치지 않습니다.
mcrute

mcrute-나는 너무 빨리 (그리고 틀리게) 말했지만, 당신은 절대적으로 옳습니다.
시간

이것은 특히 __eq__의지가 선언 될 때 매우 좋은 해결책 입니다 CommonEqualityMixin(다른 답변 참조). SQLAlchemy의 Base에서 파생 된 클래스 인스턴스를 비교할 때 특히 유용합니다. 비교하지 않기 위해로 _sa_instance_state변경 key.startswith("__")):했습니다 key.startswith("_")):. 또한 그들에 대한 역 참조가 있었고 Algorias의 대답은 끝없는 재귀를 생성했습니다. 그래서 '_'비교하는 동안 건너 뛸 수 있도록 모든 역 참조를 시작했습니다 . 참고 : Python 3.x에서로 변경 iteritems()하십시오 items().
Wookie88

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