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

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

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


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.


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

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

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.


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


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

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.


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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.