Python, czy powinienem zaimplementować operator ne () w oparciu o eq?

Mam klasę, w której chcę nadpisać operator __eq__(). Wydaje się sensowne, że powinienem nadpisać operator __ne__(), ale czy ma sens zaimplementować __ne__ w oparciu o __eq__ jako taki?

class A:
    def __eq__(self, other):
        return self.value == other.value

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

Czy jest coś, czego mi brakuje w sposobie, w jaki Python używa tych operatorów, co sprawia, że nie jest to dobry pomysł?

Author: Aaron Hall, 2010-12-04

5 answers

Tak, w porządku. W rzeczywistości dokumentacja zachęca cię do zdefiniowania __ne__ kiedy zdefiniujesz __eq__:

Nie ma żadnych domniemanych związków wśród operatorów porównawczych. Na prawda x==y nie oznacza, że x!=y jest fałszywa. Odpowiednio, przy definiowaniu __eq__(), Należy również zdefiniować __ne__() tak, aby operatory zachowywały się zgodnie z oczekiwaniami.

W wielu przypadkach (np. w tym) będzie to tak proste jak negacja wyniku __eq__, ale nie zawsze.

 46
Author: Daniel DiPaolo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-04 06:26:22

Python, czy powinienem zaimplementować __ne__() operator na podstawie __eq__?

Krótka Odpowiedź: Nie. Użyj == zamiast __eq__

W Pythonie 3, != jest negacją == domyślnie, więc nie musisz nawet pisać __ne__, a dokumentacja nie jest już opiniowana na temat pisania jednego.

Ogólnie rzecz biorąc, dla kodu Pythona 3, nie pisz go, chyba że musisz przesłonić implementację nadrzędną, np. dla wbudowanego obiekt.

Czyli pamiętaj komentarz Raymonda Hettingera :

Metoda __ne__ następuje automatycznie z __eq__ tylko wtedy, gdy {[21] }nie jest jeszcze zdefiniowana w klasie nadrzędnej. Więc, jeśli jesteś dziedziczenie z wbudowanego, najlepiej zastąpić oba.

Jeśli chcesz, aby Twój kod działał w Pythonie 2, postępuj zgodnie z zaleceniami dla Pythona 2 i będzie działał w Pythonie 3.

W Pythonie 2 Sam Python nie automatycznie zaimplementuj dowolną operację w kategoriach innej-dlatego powinieneś zdefiniować __ne__ w kategoriach == zamiast __eq__. E. G.

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

Zobacz dowód, że

  • implementacja __ne__() operatora na podstawie __eq__ i
  • brak implementacji __ne__ w Pythonie 2 w ogóle

Przedstawia nieprawidłowe zachowanie w poniższej demonstracji.

Długa Odpowiedź

Dokumentacja dla Pythona 2 mówi:

Są brak domniemanych powiązań między operatorami porównania. Na prawda x==y nie oznacza, że x!=y jest fałszywa. Odpowiednio, gdy definiowanie __eq__(), należy również zdefiniować __ne__(), aby operatorzy będą zachowywać się zgodnie z oczekiwaniami.

Oznacza to, że jeśli zdefiniujemy __ne__ w kategoriach odwrotności __eq__, możemy uzyskać spójne zachowanie.

Ta sekcja dokumentacji została zaktualizowana dla Pythona 3:

Domyślnie, __ne__() delegatów do __eq__() i odwraca wynik chyba że jest NotImplemented.

I w "Co nowego" widzimy, że to zachowanie się zmieniło:

  • != teraz zwraca przeciwieństwo ==, chyba że == zwraca NotImplemented.

do implementacji __ne__, wolimy używać operatora == zamiast używać metody __eq__ bezpośrednio tak, że jeśli self.__eq__(other) podklasy zwróci NotImplemented dla sprawdzonego typu, Python odpowiednio sprawdzi other.__eq__(self) z dokumentacji :

Obiekt NotImplemented

Ten typ ma pojedynczą wartość. Istnieje jeden obiekt o tej wartości. Dostęp do tego obiektu jest możliwy poprzez wbudowaną nazwę NotImplemented. Metody numeryczne i bogate metody porównawcze mogą zwracać wartość ta, Jeśli nie implementują operacji dla operandów pod warunkiem. (Interpreter spróbuje następnie operacji odbicia, lub jakiś inny awaryjny, w zależności od centrala.) Jego wartość jest prawda.

Python sprawdza, czy other jest podtypem, a jeśli ma zdefiniowany operator, najpierw używa metody other (odwrotność dla <, <=, >= i >). Jeśli zwracane jest NotImplemented, to używa metody odwrotnej. (Sprawdza Nie dwukrotnie tę samą metodę.) Za pomocą operatora == pozwala na to, aby logika ta miejsce.

Oczekiwania

Semantycznie powinieneś zaimplementować __ne__ pod względem sprawdzania równości, ponieważ użytkownicy Twojej klasy będą oczekiwać, że następujące funkcje będą równoważne dla wszystkich instancji A.:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

Oznacza to, że obie powyższe funkcje powinny zawsze zwracać ten sam wynik. Ale to zależy od programisty.

Demonstracja nieoczekiwanego zachowania przy definiowaniu __ne__ na podstawie __eq__:

First the konfiguracja:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

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

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

Instancje nieujemne:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

Oczekiwane Zachowanie:

(Uwaga: podczas gdy co drugie twierdzenie każdego z poniższych jest równoważne i dlatego logicznie zbędne do poprzedniego, włączam je, aby wykazać, że porządek nie ma znaczenia, gdy jedno jest podklasą drugiego.)

Te instancje mają __ne__ zaimplementowane z ==:

>>> assert not right1 == right2
>>> assert not right2 == right1
>>> assert right1 != right2
>>> assert right2 != right1

Te instancje, testujące pod Pythonem 3, również działają poprawnie:

>>> assert not right_py3_1 == right_py3_2
>>> assert not right_py3_2 == right_py3_1
>>> assert right_py3_1 != right_py3_2
>>> assert right_py3_2 != right_py3_1

I Przypomnijmy, że mają __ne__ zaimplementowane z __eq__ - chociaż jest to oczekiwane zachowanie, implementacja jest nieprawidłowa:

>>> assert not wrong1 == wrong2         # These are contradicted by the
>>> assert not wrong2 == wrong1         # below unexpected behavior!

Nieoczekiwane Zachowanie:

Zauważ, że to porównanie jest sprzeczne z powyższymi porównaniami (not wrong1 == wrong2).

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

I,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Nie pomijaj __ne__ w Pythonie 2

Dowód na to, że nie powinieneś pomijać implementacji __ne__ w Pythonie 2, Zobacz te odpowiedniki obiekty:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

Powyższy wynik powinien być False!

Python 3 source

Domyślna implementacja CPython dla __ne__ jest w typeobject.c w object_richcompare:

    case Py_NE:
        /* By default, __ne__() delegates to __eq__() and inverts the result,
           unless the latter returns NotImplemented. */
        if (self->ob_type->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*self->ob_type->tp_richcompare)(self, other, Py_EQ);
        if (res != NULL && res != Py_NotImplemented) {
            int ok = PyObject_IsTrue(res);
            Py_DECREF(res);
            if (ok < 0)
                res = NULL;
            else {
                if (ok)
                    res = Py_False;
                else
                    res = Py_True;
                Py_INCREF(res);
            }
        }

Tutaj widzimy

Ale domyślne __ne__ używa __eq__?

Domyślna implementacja Pythona 3 __ne__ na poziomie C używa __eq__, ponieważ wyższy poziom == (PyObject_RichCompare ) byłby mniej wydajny - dlatego też musi obsługiwać NotImplemented.

Jeśli __eq__ jest poprawnie zaimplementowana, to negacja == jest również poprawna - i pozwala nam uniknąć szczegółów implementacji niskiego poziomu w naszym __ne__.

Użycie == pozwala nam zachować naszą logikę niskiego poziomu wjednym miejscu iunikać adresowania w NotImplemented}.

Można błędnie założyć, że == może powrócić NotImplemented.

W rzeczywistości używa tej samej logiki co Domyślna implementacja __eq__, która sprawdza tożsamość (zobacz do_richcompare i nasze dowody poniżej)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

I porównania:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

Wydajność

Nie wierz mi na słowo, zobaczmy co jest bardziej wydajne:

class CLevel:
    "Use default logic programmed in C"

class HighLevelPython:
    def __ne__(self, other):
        return not self == other

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

Myślę, że te numery wydajności mówią same za siebie:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

Ma to sens, gdy weźmiemy pod uwagę, że low_level_python robi logikę w Pythonie, która w przeciwnym razie byłaby obsługiwana na poziomie C.

Podsumowanie

Dla Pythona 2 kompatybilnego kod, użyj == do implementacji __ne__. Jest więcej:

  • poprawne
  • proste
  • performant

Tylko w Pythonie 3 używaj negacji niskiego poziomu na poziomie C-jest ona nawet bardziej prosta i wydajna (chociaż programista jest odpowiedzialny za ustalenie, że jest poprawna).

Do Nie zapisuj logikę niskiego poziomu w Pythonie wysokiego poziomu.

 84
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-10 19:51:31

Dla przypomnienia, kanonicznie poprawny i krzyż Py2 / Py3 portable__ne__ wyglądałby następująco:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Działa to z dowolnymi __eq__, które możesz zdefiniować, i w przeciwieństwie do not (self == other), nie ingeruje w niektóre irytujące / złożone przypadki obejmujące porównania między instancjami, w których jedna instancja jest podklasą drugiej. Jeśli twój __eq__ nie używa NotImplemented zwraca, to działa (z bezsensownym nagłówkiem), jeśli czasami używa NotImplemented, to obsługuje go poprawnie. A Sprawdzanie wersji Pythona oznacza jeśli klasa jest import - ed w Pythonie 3, {[1] } jest undefined, co pozwala na natywny, efektywny powrót Pythona __ne__ implementacja (wersja C powyższego) do przejęcia.

 4
Author: ShadowRanger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-11 22:13:58

Jeśli wszystkie __eq__, __ne__, __lt__, __ge__, __le__, i __gt__ mają sens dla klasy, a następnie zamiast tego zaimplementuj __cmp__. W przeciwnym razie rób to, co robisz, ze względu na kawałek, który powiedział Daniel DiPaolo (podczas gdy ja go testowałem zamiast szukać;)) {]}

 -1
Author: Karl Knechtel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-12-04 06:28:43

Krótka odpowiedź: Tak (ale przeczytaj dokumentację, aby zrobić to dobrze)

Choć interesująca, odpowiedź Aarona Halla nie jest prawidłową metodą implementacji __ne__, ponieważ przy implementacji not self == other, metoda __ne__ drugiego operanda nigdy nie jest brana pod uwagę. W przeciwieństwie do tego, jak pokazano poniżej, Domyślna implementacja Pythona 3 metody __ne__ operandu wykonuje fallback na metodzie __ne__ drugiego operandu zwracając NotImplemented, gdy jej metoda __eq__ zwraca NotImplemented. ShadowRanger podał poprawną implementację metody __ne__:

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

Implementacja operatorów porównania

Odniesienie do języka Python dla Pythona 3 stany w jego rozdział III model danych :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Są to tak zwane "bogate metody porównawcze". Korespondencja między symbolami operatora a metodą nazwy są następujące: x<y wywołania x.__lt__(y), x<=y połączenia x.__le__(y), x==y połączenia x.__eq__(y), x!=y rozmowy x.__ne__(y), x>y wywołania x.__gt__(y) i x>=y połączenia x.__ge__(y).

Rich comparison method may return the singleton NotImplemented if nie implementuje operacji dla danej pary argumentów.

Nie ma wersji podmienionych argumentów tych metod (do użycia gdy lewy argument nie wspiera operacji, ale prawy argument robi); raczej __lt__() i __gt__() są sobie nawzajem odbicie, __le__() i __ge__() są wzajemnie odbiciem i __eq__() i __ne__() są ich własnym odbiciem. Jeśli operands są różnych typów, a właściwy typ operanda to bezpośredni lub podklasa pośrednia typu lewego operanda, metoda odbicia prawy operand ma pierwszeństwo, w przeciwnym razie metoda lewego operanda ma pierwszeństwo. Wirtualne podklasowanie nie jest brane pod uwagę.

Tłumaczenie tego na kod Pythona (operator_eq dla ==, operator_ne na !=, operator_lt na <, operator_gt na >, operator_le dla <= i operator_ge dla >=):

def operator_eq(left, right):
    if isinstance(right, type(left)):
        result = right.__eq__(left)

        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)

        if result is NotImplemented:
            result = right.__eq__(left)

    if result is NotImplemented:
        result = left is right

    return result


def operator_ne(left, right):
    if isinstance(right, type(left)):
        result = right.__ne__(left)

        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)

        if result is NotImplemented:
            result = right.__ne__(left)

    if result is NotImplemented:
        result = left is not right

    return result


def operator_lt(left, right):
    if isinstance(right, type(left)):
        result = right.__gt__(left)

        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)

        if result is NotImplemented:
            result = right.__gt__(left)

    if result is NotImplemented:
        raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_gt(left, right):
    if isinstance(right, type(left)):
        result = right.__lt__(left)

        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)

        if result is NotImplemented:
            result = right.__lt__(left)

    if result is NotImplemented:
        raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_le(left, right):
    if isinstance(right, type(left)):
        result = right.__ge__(left)

        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)

        if result is NotImplemented:
            result = right.__ge__(left)

    if result is NotImplemented:
        raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_ge(left, right):
    if isinstance(right, type(left)):
        result = right.__le__(left)

        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)

        if result is NotImplemented:
            result = right.__le__(left)

    if result is NotImplemented:
        raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result

Domyślna implementacja metod porównawczych

Dokumentacja dodaje:

Domyślnie, __ne__() deleguje na __eq__() i odwraca wynik chyba że jest NotImplemented. Nie ma innych domniemanych zależności między operatorami porównania, np. prawda z (x<y or x==y) nie oznacza x<=y.

Domyślna implementacja metody porównawcze (__eq__, __ne__, __lt__, __gt__, __le__ i __ge__) można zatem podać przez:

def __eq__(self, other):
    return NotImplemented

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

Jest to więc prawidłowa implementacja metody __ne__. Nie zawsze Zwraca odwrotność metody __eq__, ponieważ gdy metoda __eq__ zwraca NotImplemented, jej odwrotność not NotImplemented wynosi False (ponieważ bool(NotImplemented) jest True) zamiast pożądanego NotImplemented.

Niepoprawne implementacje __ne__

Jak pokazał Aaron Hall, not self.__eq__(other) nie jest poprawna wdrożenie metody __ne__. ale nie jest not self == other.[165]} to ostatnie zostało zademonstrowane poniżej, porównując zachowanie domyślnej implementacji z zachowaniem not self == other implementacji w dwóch przypadkach:

  • metoda __eq__ zwraca NotImplemented;
  • metoda __eq__ Zwraca wartość inną niż NotImplemented.

Domyślna implementacja

Zobaczmy, co się stanie, gdy metoda A.__ne__ użyje domyślnej implementacji i A.__eq__ metoda zwraca NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. != połączenia A.__ne__.
  2. A.__ne__ połączenia A.__eq__.
  3. A.__eq__ zwraca NotImplemented.
  4. != połączenia B.__ne__.
  5. B.__ne__ zwraca "B.__ne__".

Pokazuje to, że gdy metoda A.__eq__ zwróci NotImplemented, metoda A.__ne__ powróci do metody B.__ne__.

Teraz zobaczmy, co się stanie, gdy metoda A.__ne__ użyje domyślnej implementacji, a metoda A.__eq__ zwróci inną wartość from NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. != połączenia A.__ne__.
  2. A.__ne__ połączenia A.__eq__.
  3. A.__eq__ zwraca True.
  4. != zwraca not True, czyli False.

Pokazuje to, że w tym przypadku metoda A.__ne__ Zwraca odwrotność metody A.__eq__. W ten sposób metoda __ne__ zachowuje się jak reklamowana w dokumentacji.

Zastąpienie domyślnej implementacji metody A.__ne__ poprawną implementacją podaną powyżej daje te same wyniki.

not self == other realizacja

Zobaczmy, co się stanie, gdy zastąpienie domyślnej implementacji metody A.__ne__ implementacją not self == other i metodą A.__eq__ zwróci NotImplemented:

class A:

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. != połączenia A.__ne__.
  2. A.__ne__ połączenia ==.
  3. == połączenia A.__eq__.
  4. A.__eq__ zwraca NotImplemented.
  5. == połączenia B.__eq__.
  6. B.__eq__ zwraca NotImplemented.
  7. == zwraca A() is B(), czyli False.
  8. A.__ne__ zwraca not False, czyli True.

Domyślna implementacja metody __ne__ zwróciła "B.__ne__", a nie True.

Teraz zobaczmy, co się stanie, gdy zastąpi domyślną implementację metody A.__ne__ implementacją not self == other i metoda A.__eq__ zwróci wartość inną niż NotImplemented:

class A:

    def __eq__(self, other):
        return True

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. != połączenia A.__ne__.
  2. A.__ne__ połączenia ==.
  3. == rozmowy A.__eq__.
  4. A.__eq__ zwraca True.
  5. A.__ne__ zwraca not True, czyli False.

Domyślna implementacja metody __ne__ również zwróciła False w tym przypadku.

ponieważ implementacja ta nie powtarza zachowania domyślnej implementacji metody __ne__, gdy metoda __eq__ zwraca NotImplemented, jest niepoprawna.

 -1
Author: Maggyero,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-03 08:26:47