Eleganckie sposoby wspierania równoważności („równości”) w klasach Python

421

Podczas pisania klas niestandardowych często ważne jest, aby umożliwić równoważność za pomocą operatorów ==i !=. W Pythonie jest to możliwe dzięki zastosowaniu odpowiednio metod specjalnych __eq__i __ne__. Najłatwiejszym sposobem, aby to zrobić, jest następująca metoda:

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)

Czy znasz bardziej elegancki sposób na zrobienie tego? Czy znasz jakieś szczególne wady korzystania z powyższej metody porównywania __dict__?

Uwaga : Trochę wyjaśnienia - kiedy __eq__i __ne__są niezdefiniowane, znajdziesz to zachowanie:

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

Oznacza to, że a == bocenia, Falseponieważ tak naprawdę działa a is b, test tożsamości (tj. „Czy aten sam obiekt jak b?”).

Kiedy __eq__i __ne__są zdefiniowane, znajdziesz to zachowanie (którego właśnie szukamy):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
gotgenes
źródło
6
+1, ponieważ nie wiedziałem, że dykt używał równości członków dla ==, założyłem, że policzył je tylko dla dykt dla tych samych obiektów. Myślę, że jest to oczywiste, ponieważ Python ma isoperator odróżniający tożsamość obiektu od porównywania wartości.
SingleNegationElimination
5
Myślę, że zaakceptowana odpowiedź zostanie poprawiona lub ponownie przypisana do odpowiedzi Algoriasa, aby zaimplementować ścisłą kontrolę typu.
maks.
1
Upewnij się także, że hash jest nadpisany stackoverflow.com/questions/1608842/…
Alex Punnen

Odpowiedzi:

328

Rozważ ten prosty problem:

class Number:

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


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

n1 == n2 # False -- oops

Tak więc Python domyślnie używa identyfikatorów obiektów do operacji porównania:

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

Przesłonięcie __eq__funkcji wydaje się rozwiązać problem:

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

W Pythonie 2 zawsze pamiętaj o zastąpieniu __ne__funkcji, ponieważ dokumentacja stwierdza:

Nie ma żadnych domniemanych relacji między operatorami porównania. Prawda x==ynie sugeruje, że x!=yjest to fałsz. W związku z tym podczas definiowania __eq__()należy również zdefiniować, __ne__()aby operatorzy zachowywali się zgodnie z oczekiwaniami.

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


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

W Pythonie 3 nie jest to już konieczne, ponieważ dokumentacja stwierdza:

Domyślnie __ne__()deleguje __eq__()i odwraca wynik, chyba że tak jest NotImplemented. Nie ma innych domniemanych związków między operatorami porównania, na przykład prawda (x<y or x==y)nie implikuje x<=y.

Ale to nie rozwiązuje wszystkich naszych problemów. Dodajmy podklasę:

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

Uwaga: Python 2 ma dwa rodzaje klas:

  • w stylu klasycznym (lub starym stylu ) klas, które mają nie dziedziczyć zobjecti które zostały zadeklarowane jakoclass A:,class A():lubclass A(B):gdzieBjest klasa stylu klasycznym;

  • klasy w nowym stylu , które dziedzicząobjecti są zadeklarowane jakoclass A(object)lubclass A(B):gdzieBjest klasa w nowym stylu. Python 3 ma tylko klasy w nowym stylu, które są zadeklarowane jakoclass A:,class A(object):lubclass A(B):.

W przypadku klas klasycznych operacja porównania zawsze wywołuje metodę pierwszego operandu, podczas gdy w klasach nowego stylu zawsze wywołuje metodę operandu podklasy, niezależnie od kolejności operandów .

A więc, jeśli Numberjest to klasa w stylu klasycznym:

  • n1 == n3połączenia n1.__eq__;
  • n3 == n1połączenia n3.__eq__;
  • n1 != n3połączenia n1.__ne__;
  • n3 != n1połączenia n3.__ne__.

A jeśli Numberjest klasą w nowym stylu:

  • zarówno n1 == n3i n3 == n1wezwanie n3.__eq__;
  • zarówno n1 != n3i n3 != n1zadzwonić n3.__ne__.

Aby rozwiązać problem nieprzemienności operatorów ==i !=dla klas klasycznych języka Python 2, metody __eq__i __ne__powinny zwracać NotImplementedwartość, gdy typ argumentu nie jest obsługiwany. Dokumentacja definiuje NotImplementedwartość jako:

Metody numeryczne i metody porównywania rozszerzonego mogą zwrócić tę wartość, jeśli nie implementują operacji dla podanych argumentów. (Tłumacz interpretuje następnie operację odbicia lub inną operację zastępczą, w zależności od operatora). Jej wartość prawdy jest prawdziwa.

W tym przypadku delegaci operator operacja porównaniu do odzwierciedlenie metody z innego argumentu. W dokumentacji definiuje odzwierciedlenie metody jak:

Nie ma wersji tych metod wymiany argumentów (do użycia, gdy lewy argument nie obsługuje operacji, ale prawy argument tak;); raczej __lt__()i __gt__()są wzajemnym odbiciem, __le__()i __ge__()są wzajemnym odbiciem, __eq__()i __ne__()są ich własnym odbiciem.

Wynik wygląda następująco:

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

Zwracanie NotImplementedwartości zamiast Falsejest słuszne, nawet dla klas w nowym stylu jeśli przemienność z ==i !=jest pożądane operatorów, gdy argumenty są niepowiązanych typów (bez spadku).

Czy już dotarliśmy? Nie do końca. Ile mamy unikalnych numerów?

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

Zestawy używają skrótów obiektów, a domyślnie Python zwraca skrót identyfikatora obiektu. Spróbujmy to zastąpić:

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

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

Wynik końcowy wygląda następująco (dodałem na końcu kilka stwierdzeń do weryfikacji):

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
Tal Weiss
źródło
3
hash(tuple(sorted(self.__dict__.items())))nie będzie działać, jeśli wśród wartości obiektu znajdują się obiekty, których nie da się ukryć self.__dict__(tzn. jeśli którykolwiek z atrybutów obiektu jest ustawiony na, powiedzmy, a list).
maks.
3
To prawda, ale jeśli masz takie zmienne obiekty w swoich warach (), dwa obiekty nie są tak naprawdę równe ...
Tal Weiss
12
Świetne podsumowanie, ale powinieneś wdrożyć __ne__za pomocą ==zamiast__eq__ .
Florian Brucker
1
Trzy uwagi: 1. W Pythonie 3 nie trzeba __ne__już implementować : „Domyślnie __ne__()deleguje __eq__()i odwraca wynik, chyba że jest NotImplemented”. 2. Jeśli ktoś nadal chce wdrożyć __ne__, bardziej rodzajowe realizacja (jeden używany przez Pythonie 3 myślę) wynosi: x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Podane __eq__i __ne__implementacje są nieoptymalne: if isinstance(other, type(self)):daje 22 __eq__i 10 __ne__wywołań, a if isinstance(self, type(other)):dawałoby 16 __eq__i 6 __ne__wywołań.
Maggyero,
4
Zapytał o elegancję, ale stał się solidny.
GregNash,
201

Trzeba zachować ostrożność przy dziedziczeniu:

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

Dokładniej sprawdź typy, takie jak to:

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

Poza tym twoje podejście będzie działać dobrze, po to są specjalne metody.

Algorias
źródło
To dobra uwaga. Przypuszczam, że warto zauważyć, że wbudowane podklasy nadal pozwalają na równość w obu kierunkach, więc sprawdzenie, czy jest to ten sam typ, może nawet być niepożądane.
gotgenes
12
Sugerowałbym zwrócić NotImplemented, jeśli typy są różne, przekazując porównanie do rh.
maks.
4
@max porównanie niekoniecznie jest wykonywane od lewej strony (LHS) do prawej strony (RHS), a następnie od RHS do LHS; patrz stackoverflow.com/a/12984987/38140 . Mimo to powrót NotImplementedzgodnie z sugestią zawsze spowoduje superclass.__eq__(subclass)pożądane zachowanie.
gotgenes
4
Jeśli masz mnóstwo członków i nie ma zbyt wielu kopii obiektów siedzących w pobliżu, zazwyczaj dobrze jest dodać wstępny test tożsamości if other is self. Pozwala to uniknąć dłuższego porównywania słowników i może być ogromną oszczędnością, gdy obiekty są używane jako klucze słowników.
Dane White
2
I nie zapomnij wdrożyć__hash__()
Dane White
161

Opisujesz sposób, w jaki zawsze to robiłem. Ponieważ jest on całkowicie ogólny, zawsze możesz podzielić tę funkcjonalność na klasę mixin i odziedziczyć ją w klasach, w których chcesz tę funkcjonalność.

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
cdleary
źródło
6
+1: Wzór strategii, aby umożliwić łatwą wymianę w podklasach.
S.Lott,
3
isinstance jest do bani. Po co to sprawdzać? Dlaczego nie po prostu siebie .__ dict__ == inny .__ dict__?
nosklo
3
@nosklo: Nie rozumiem .. co jeśli dwa obiekty z zupełnie niezwiązanych klas mają te same atrybuty?
maks.
1
Myślałem, że nokslo zasugerował, że pominięcie to jest przypadek. W takim przypadku nie wiesz już, czy othernależy do podklasy self.__class__.
maks.
10
Innym problemem związanym z __dict__porównaniem jest to, że masz atrybut, którego nie chcesz brać pod uwagę w definicji równości (na przykład unikalny identyfikator obiektu lub metadane, takie jak znacznik utworzony w czasie).
Adam Parkin
14

Nie była to bezpośrednia odpowiedź, ale wydawała się na tyle istotna, że ​​można się nią zająć, ponieważ czasami oszczędza to trochę nudnego nudy. Wytnij prosto z dokumentów ...


funkools.total_ordering (cls)

Biorąc pod uwagę klasę definiującą jedną lub więcej bogatych metod porządkowania porównań, ten dekorator klas dostarcza resztę. Upraszcza to wysiłek związany z określeniem wszystkich możliwych bogatych operacji porównania:

Klasa musi określić jeden __lt__(), __le__(), __gt__(), lub __ge__(). Ponadto klasa powinna podać __eq__()metodę.

Nowości w wersji 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()))
John Mee
źródło
1
Jednak total_ordering ma subtelne pułapki: regebro.wordpress.com/2010/12/13/… . Być świadomym !
Mr_and_Mrs_D
8

Nie musisz zastępować obu __eq__i __ne__możesz tylko zastąpić, __cmp__ale będzie to miało wpływ na wynik ==,! ==, <,> i tak dalej.

istesty tożsamości obiektu. Oznacza to, że isb będzie miało miejsce Truew przypadku, gdy oba a i b utrzymują odniesienie do tego samego obiektu. W Pythonie zawsze przechowujesz odwołanie do obiektu w zmiennej, a nie do rzeczywistego obiektu, więc w gruncie rzeczy a jest prawdą b, obiekty w nich powinny znajdować się w tym samym miejscu w pamięci. W jaki sposób i co najważniejsze, dlaczego miałby pan obchodzić to zachowanie?

Edycja: Nie wiedziałem, że __cmp__został usunięty z Pythona 3, więc unikaj go.

Vasil
źródło
Ponieważ czasami masz inną definicję równości swoich obiektów.
Ed S.,
operator is daje tłumaczom odpowiedź na tożsamość obiektu, ale nadal możesz wyrazić swoje poglądy na temat równości, zastępując cmp
Vasil,
7
W Pythonie 3: „Funkcja cmp () zniknęła, a specjalna metoda __cmp __ () nie jest już obsługiwana”. is.gd/aeGv
gotgenes
4

Z tej odpowiedzi: https://stackoverflow.com/a/30676267/541136 Udowodniłem to, chociaż poprawne jest definiowanie __ne__w kategoriach __eq__- zamiast

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

powinieneś użyć:

def __ne__(self, other):
    return not self == other
Aaron Hall
źródło
2

Myślę, że dwa warunki, których szukasz, to równość (==) i tożsamość (jest). Na przykład:

>>> 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
za dużo php
źródło
1
Być może oprócz tego, że można stworzyć klasę, która porównuje tylko dwa pierwsze elementy na dwóch listach, a jeśli te elementy są równe, wartość jest równa Prawda. Myślę, że to równoważność, a nie równość. Nadal doskonale poprawne w eq .
gotgenes,
Zgadzam się jednak, że „jest” to test tożsamości.
gotgenes
1

Test „jest” sprawdzi tożsamość za pomocą wbudowanej funkcji „id ()”, która zasadniczo zwraca adres pamięci obiektu, a zatem nie jest przeciążalna.

Jednak w przypadku testowania równości klasy prawdopodobnie chcesz być nieco bardziej rygorystyczny w stosunku do swoich testów i porównywać tylko atrybuty danych w swojej klasie:

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

Ten kod będzie porównywał tylko niefunkcjonalne dane członków twojej klasy, a także pomija wszystko prywatne, co jest generalnie tym, czego chcesz. W przypadku Plain Old Python Objects mam klasę bazową, która implementuje __init__, __str__, __repr__ i __eq__, więc moje obiekty POPO nie przenoszą ciężaru całej tej dodatkowej (i w większości przypadków identycznej) logiki.

mcrute
źródło
Bit nitpicky, ale testy „is” używają id () tylko wtedy, gdy nie zdefiniowano własnej funkcji członkowskiej is_ () (2.3+). [ docs.python.org/library/operator.html]
spędził
Zakładam, że przez „przesłonięcie” masz na myśli załatanie małpiego modułu operatora. W takim przypadku twoje oświadczenie nie jest całkowicie dokładne. Moduł operatorów został udostępniony dla wygody, a nadpisanie tych metod nie wpływa na zachowanie operatora „jest”. Porównanie za pomocą „jest” zawsze używa do porównania id () obiektu, tego zachowania nie można zastąpić. Również funkcja członka is_ nie ma wpływu na porównanie.
mcrute,
mcrute - Mówiłem za wcześnie (i niepoprawnie), masz absolutną rację.
spędził
Jest to bardzo fajne rozwiązanie, zwłaszcza gdy __eq__zostanie zadeklarowane w CommonEqualityMixin(patrz inna odpowiedź). Uważam to za szczególnie przydatne podczas porównywania instancji klas pochodzących z Base w SQLAlchemy. Aby nie porównywać _sa_instance_state, zmieniłem key.startswith("__")):na key.startswith("_")):. Miałem też w sobie pewne referencje, a odpowiedź Algoriasa generowała nieskończoną rekurencję. Wymieniłem '_'więc wszystkie odwołania wsteczne na początku , aby były one pomijane podczas porównywania. UWAGA: w Python 3.x zmień iteritems()na items().
Wookie88
@mcrute Zazwyczaj __dict__instancja nie ma niczego, co zaczyna się od __niej, chyba że zostało zdefiniowane przez użytkownika. Rzeczy takie jak __class__, __init__itp nie są w instancji __dict__, ale w swojej klasie __dict__. OTOH, prywatne atrybuty można łatwo zacząć __i prawdopodobnie powinny być używane __eq__. Czy możesz wyjaśnić, czego dokładnie próbowałeś uniknąć, pomijając __atrybuty z prefiksem?
maks.
1

Zamiast używać subclassing / mixins, lubię używać ogólnego dekoratora klas

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

Stosowanie:

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

a = Number(1)
b = Number(1)
assert a == b
bluenote10
źródło
0

Obejmuje to komentarze do odpowiedzi Algoriasa i porównuje obiekty według jednego atrybutu, ponieważ nie dbam o cały dyktando. hasattr(other, "id")musi być prawdą, ale wiem, że dzieje się tak, ponieważ ustawiłem to w konstruktorze.

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
rev Noumenon
źródło