Mam klasę, w której chcę zastąpić __eq__
metodę. Wydaje się sensowne, że powinienem zastąpić __ne__
metodę jak dobrze, ale czy to ma sens, aby wdrożyć __ne__
w kategoriach __eq__
jako takich?
class A:
def __init__(self, attr):
self.attr = attr
def __eq__(self, other):
return self.attr == other.attr
def __ne__(self, other):
return not self.__eq__(other)
A może jest coś, czego mi brakuje w sposobie, w jaki Python używa tych metod, co sprawia, że nie jest to dobry pomysł?
python
comparison
operators
python-datamodel
Falmarri
źródło
źródło
__ne__
using__eq__
, a jedynie do jej wdrożenia.NotImplemented
powrót z jednej strony jako wskazówkę do delegowania__ne__
z drugiej strony,not self == other
jest (zakładając, że operand__eq__
nie wie, jak porównać inny operand), niejawnie deleguje na__eq__
drugą stronę, a następnie odwraca. W przypadku dziwnych typów, np. Pól ORM SQLAlchemy, powoduje to problemy .__ne__
automatycznie przekazywane,__eq__
a cytat w tej odpowiedzi nie istnieje już w docs. Podsumowując, wdrażanie tylko__eq__
i zezwalanie na__ne__
delegowanie jest całkowicie pythonowe .Krótka odpowiedź: Nie wdrażaj tego, ale jeśli musisz
==
, nie używaj__eq__
W Pythonie 3
!=
jest negacją==
domyślnie, więc nie musisz nawet pisać a__ne__
, a dokumentacja nie jest już zdania na temat pisania takiego.Ogólnie rzecz biorąc, w przypadku kodu tylko w Pythonie 3 nie pisz go, chyba że musisz przyćmić implementację rodzica, np. Dla wbudowanego obiektu.
To znaczy, pamiętaj o komentarzu Raymonda Hettingera :
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 implementuje automatycznie żadnej operacji w kategoriach innej - dlatego powinieneś zdefiniować
__ne__
w kategoriach==
zamiast__eq__
. NA PRZYKŁADclass 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
__ne__()
operator wdrażający oparty na__eq__
i__ne__
w Pythonie 2zapewnia nieprawidłowe zachowanie w poniższej demonstracji.
Długa odpowiedź
Dokumentacji dla Pythona 2 mówi:
Oznacza to, że jeśli zdefiniujemy
__ne__
w kategoriach odwrotności do__eq__
, możemy uzyskać spójne zachowanie.Ta sekcja dokumentacji została zaktualizowana dla języka Python 3:
aw sekcji „co nowego” widzimy, że zmieniło się to zachowanie:
Do implementacji
__ne__
wolimy używać==
operatora zamiast bezpośrednio używać__eq__
metody, więc jeśliself.__eq__(other)
podklasa zwróciNotImplemented
dla sprawdzonego typu, Python odpowiednio sprawdziother.__eq__(self)
Z dokumentacji :Kiedy podano bogaty operator porównania, jeśli nie są one tego samego typu, Python sprawdza czy
other
jest podtypem, a jeśli ma to operator zdefiniowany, używaother
pierwszy „s metody (odwrotność do<
,<=
,>=
i>
). JeśliNotImplemented
jest zwracany, a następnie wykorzystuje metodę Przeciwieństwem jest. (To ma nie sprawdzić tej samej metody dwa razy). Za pomocą==
operatora pozwala na to logika się odbyć.Oczekiwania
Z semantycznego
__ne__
punktu widzenia należy zaimplementować w zakresie sprawdzania równości, ponieważ użytkownicy Twojej klasy będą oczekiwać, że następujące funkcje będą równoważne dla wszystkich wystąpień 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 podczas definiowania
__ne__
na podstawie__eq__
:Najpierw 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."""
Utwórz instancje nie równoważne:
right1, right2 = ComparableRight(1), ChildComparableRight(2) wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2) right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Spodziewane zachowanie:
(Uwaga: chociaż co drugie stwierdzenie każdego z poniższych jest równoważne, a zatem logicznie nadmiarowe w stosunku do poprzedniego, dołączam je, aby wykazać, że kolejność nie ma znaczenia, gdy jedno jest podklasą drugiego. )
Te wystąpienia zostały
__ne__
zaimplementowane z==
:assert not right1 == right2 assert not right2 == right1 assert right1 != right2 assert right2 != right1
Te instancje, testowane w Pythonie 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
Przypomnijmy, że zostały one
__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 porównaniami powyżej (
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 2Aby dowiedzieć się, że nie należy pomijać implementacji
__ne__
w Pythonie 2, zobacz te równoważne 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
!Źródło Pythona 3
Domyślna implementacja CPythona dla
__ne__
znajduje siętypeobject.c
wobject_richcompare
:case Py_NE: /* By default, __ne__() delegates to __eq__() and inverts the result, unless the latter returns NotImplemented. */ if (Py_TYPE(self)->tp_richcompare == NULL) { res = Py_NotImplemented; Py_INCREF(res); break; } res = (*Py_TYPE(self)->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); } } break;
Ale domyślne
__ne__
zastosowania__eq__
?Domyślne
__ne__
szczegóły implementacji Pythona 3 na poziomie C są używane,__eq__
ponieważ wyższy poziom==
( PyObject_RichCompare ) byłby mniej wydajny - i dlatego musi również obsługiwaćNotImplemented
.Jeśli
__eq__
jest poprawnie zaimplementowany, negacja==
jest również poprawna - i pozwala nam uniknąć szczegółów implementacji niskiego poziomu w naszym__ne__
.Korzystanie
==
pozwala nam zachować logikę niskiego poziomu w jednym miejscu i uniknąć adresowaniaNotImplemented
w__ne__
.Można by 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ść (patrz 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
Występ
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 liczby 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, jeśli weźmiesz pod uwagę, że
low_level_python
w Pythonie jest wykonywana logika, która w innym przypadku byłaby obsługiwana na poziomie C.Odpowiedź na niektórych krytyków
Inny odpowiadający pisze:
Brak
__ne__
powrotuNotImplemented
nie oznacza, że jest to błędne. Zamiast tego obsługujemy priorytetyzację zaNotImplemented
pomocą sprawdzania równości z==
. Zakładając, że==
zostało poprawnie zaimplementowane, gotowe.Cóż, wyjaśnijmy to.
Jak wspomniano wcześniej, Python 3 domyślnie obsługuje
__ne__
, najpierw sprawdzając, czyself.__eq__(other)
zwracaNotImplemented
(singleton) - co powinno być sprawdzaneis
i zwracane, jeśli tak, w przeciwnym razie powinien zwrócić odwrotność. Oto logika zapisana jako mieszanka klas:class CStyle__ne__: """Mixin that provides __ne__ functionality equivalent to the builtin functionality """ def __ne__(self, other): equal = self.__eq__(other) if equal is NotImplemented: return NotImplemented return not equal
Jest to konieczne dla poprawności interfejsu API języka Python na poziomie C i zostało wprowadzone w Pythonie 3, tworząc
__ne__
metody w tej poprawki , aby zamknąć Issue 21408 i__ne__
metody w następujący sposób na czyszczenie usunięte tutajzbędny. Wszystkie odpowiednie
__ne__
metody zostały usunięte, w tym te implementujące własne sprawdzenie, a także te, które delegują__eq__
bezpośrednio lub za pośrednictwem==
- i==
był to najczęstszy sposób robienia tego.Czy symetria jest ważna?
Nasz krytyk zapewnia trwałe patologiczną przykład, aby sprawę do postępowania
NotImplemented
w__ne__
ceniąc symetrię ponad wszystko. Stwórzmy argument z jasnym przykładem:class B: """ this class has no __eq__ implementation, but asserts any instance is not equal to any other object """ def __ne__(self, other): return True class A: "This class asserts instances are equivalent to all other objects" def __eq__(self, other): return True >>> A() == B(), B() == A(), A() != B(), B() != A() (True, True, False, True)
Tak więc, zgodnie z tą logiką, aby zachować symetrię, musimy napisać skomplikowaną
__ne__
, niezależnie od wersji Pythona.class B: def __ne__(self, other): return True class A: def __eq__(self, other): return True def __ne__(self, other): result = other.__eq__(self) if result is NotImplemented: return NotImplemented return not result >>> A() == B(), B() == A(), A() != B(), B() != A() (True, True, True, True)
Najwyraźniej nie powinniśmy przejmować się tym, że te przypadki są równe i nierówne.
Proponuję, że symetria jest mniej ważna niż domniemanie rozsądnego kodu i przestrzeganie zaleceń dokumentacji.
Gdyby jednak A miał sensowną implementację
__eq__
, moglibyśmy nadal podążać za moim kierunkiem tutaj i nadal mielibyśmy symetrię:class B: def __ne__(self, other): return True class A: def __eq__(self, other): return False # <- this boolean changed... >>> A() == B(), B() == A(), A() != B(), B() != A() (False, False, True, True)
Wniosek
W przypadku kodu zgodnego z Python 2 użyj
==
do implementacji__ne__
. To jest więcej:Tylko w Pythonie 3 używaj negacji niskopoziomowej na poziomie C - jest jeszcze prostsza i bardziej wydajna (chociaż to programista jest odpowiedzialny za ustalenie, że jest poprawna ).
Ponownie, nie pisz logiki niskiego poziomu w języku Python wysokiego poziomu.
źródło
a1 != c2
wróciłFalse
--- nie uruchomića1.__ne__
, alec2.__ne__
, który negował mixin za__eq__
metodę. PonieważNotImplemented
jest prawdą,not NotImplemented
jestFalse
.not (self == other)
, ale nikt nie twierdzi, że nie jest szybki (no cóż, i tak szybszy niż jakakolwiek inna opcja w Py2). Problem polega na tym, że w niektórych przypadkach jest to złe ; Sam Python robił to kiedyśnot (self == other)
, ale zmienił się, ponieważ był niepoprawny w obecności dowolnych podklas . Odpowiedź od najszybszego do złego jest nadal błędna .__ne__
delegatów__eq__
(z obu stron, jeśli to konieczne), ale nigdy nie spada__ne__
na drugą stronę, nawet jeśli obaj__eq__
"poddają się". Prawidłowych__ne__
delegatów na swoje własne__eq__
, ale jeśli to powróciNotImplemented
, cofa się, aby przejść na drugą stronę__ne__
, zamiast odwracać drugą stronę__eq__
(ponieważ druga strona mogła nie wyrazić wyraźnej zgody na delegowanie__eq__
, a ty nie powinieneś podejmować taką decyzję za to).__eq__
ani nie__ne__
zwraca alboTrue
alboFalse
, ale raczej obiekt proxy (który okazuje się być „prawdziwy”). Nieprawidłowe wdrożenie__ne__
oznacza, że dla porównania liczy się kolejność (proxy dostajesz tylko w jednym zamówieniu).__ne__
całkowicie pominąć . Za rok Py2 będzie martwy i ignorujemy to. :-)Tak dla przypomnienia, kanonicznie poprawny i krzyżowy przenośny Py2 / Py3
__ne__
wyglądałby tak: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 każdym,
__eq__
który możesz zdefiniować:not (self == other)
, nie koliduje z niektórymi irytującymi / złożonymi przypadkami obejmującymi porównania, w których jedna z klas nie oznacza, że wynik__ne__
jest taki sam jak wyniknot
on__eq__
(np. ORM SQLAlchemy, gdzie oba__eq__
i__ne__
zwracają specjalne obiekty proxy, nieTrue
lubFalse
, a próba zwrócenianot
wyniku__eq__
zwróciFalse
zamiast prawidłowego obiektu proxy).not self.__eq__(other)
, to poprawnie delegatów na__ne__
od drugiej instancji, gdyself.__eq__
powracaNotImplemented
(not self.__eq__(other)
byłoby extra źle, boNotImplemented
jest truthy, więc gdy__eq__
nie wiedział, jak przeprowadzić porównanie,__ne__
by powrócićFalse
, co oznacza, że dwa obiekty są równe, gdy w rzeczywistości jedynym Obiekt zapytany nie miał pojęcia, co oznaczałoby brak równości)Jeśli
__eq__
nie używaszNotImplemented
zwrotów, to działa (z bezsensownym narzutem), jeśliNotImplemented
czasami używa , to obsługuje to poprawnie. A sprawdzenie wersji Pythona oznacza, że jeśli klasa jestimport
-ed w Pythonie 3,__ne__
pozostaje niezdefiniowana, co pozwala na przejęcie natywnej, wydajnej__ne__
implementacji rezerwowej Pythona (wersja C powyższego) .Dlaczego jest to potrzebne
Reguły przeciążania Pythona
Wyjaśnienie, dlaczego robisz to zamiast innych rozwiązań, jest nieco tajemnicze. Python ma kilka ogólnych zasad dotyczących przeciążania operatorów, w szczególności operatorów porównania:
LHS OP RHS
spróbujLHS.__op__(RHS)
, a jeśli to zwróciNotImplemented
, spróbujRHS.__rop__(LHS)
. Wyjątek: jeśliRHS
jest to podklasaLHS
klasy 's,RHS.__rop__(LHS)
najpierw przetestuj . W przypadku operatorów porównania,__eq__
a__ne__
są ich własne „RPO” s (tak kolejność test na__ne__
toLHS.__ne__(RHS)
, wtedyRHS.__ne__(LHS)
, odwracane, jeśliRHS
jest podklasąLHS
„s klasy)LHS.__eq__(RHS)
zwracanieTrue
nie oznaczaLHS.__ne__(RHS)
zwrotówFalse
(w rzeczywistości operatory nie są nawet zobowiązane do zwracania wartości logicznych; ORMy, takie jak SQLAlchemy, celowo tego nie robią, pozwalając na bardziej wyrazistą składnię zapytań). Od Pythona 3 domyślna__ne__
implementacja zachowuje się w ten sposób, ale nie jest to umowne; możesz nadpisać__ne__
w sposób, który nie jest ścisłym przeciwieństwem__eq__
.Jak to się ma do przeciążania komparatorów
Więc kiedy przeciążasz operatora, masz dwie prace:
NotImplemented
, aby Python mógł delegować implementację drugiego operanduProblem z
not self.__eq__(other)
def __ne__(self, other): return not self.__eq__(other)
nigdy nie deleguje na drugą stronę (i jest niepoprawna, jeśli
__eq__
poprawnie zwracaNotImplemented
). Kiedyself.__eq__(other)
zwracaNotImplemented
(co jest „prawdą”), po cichu wracaszFalse
, więcA() != something_A_knows_nothing_about
wracaFalse
, gdy powinien był sprawdzić, czysomething_A_knows_nothing_about
wie, jak porównać z wystąpieniamiA
, a jeśli nie, powinien był wrócićTrue
(ponieważ jeśli żadna ze stron nie wie jak w porównaniu z innymi, nie są sobie równe). JeśliA.__eq__
jest niepoprawnie zaimplementowane (zwracaFalse
zamiast,NotImplemented
gdy nie rozpoznaje drugiej strony), to jest to „poprawne” zA
perspektywy, zwracająceTrue
(ponieważA
nie uważa, że jest równe, więc nie jest równe), ale może być źle odsomething_A_knows_nothing_about
perspektywy, ponieważ nigdy nie pytałsomething_A_knows_nothing_about
;A() != something_A_knows_nothing_about
kończy sięTrue
, alesomething_A_knows_nothing_about != A()
możeFalse
, lub jakakolwiek inna wartość zwracana.Problem z
not self == other
def __ne__(self, other): return not self == other
jest bardziej subtelny. Będzie to poprawne dla 99% klas, w tym wszystkich klas, dla których
__ne__
jest logiczną odwrotnością__eq__
. Alenot self == other
łamie obie powyższe reguły, co oznacza, że dla klas, dla których__ne__
nie jest logiczną odwrotnością__eq__
, wyniki są ponownie niesymetryczne, ponieważ jeden z operandów nigdy nie jest pytany, czy w ogóle może zaimplementować__ne__
, nawet jeśli drugi operand nie może. Najprostszym przykładem jest klasa dziwakiem, który powracaFalse
do wszystkich porównań, więcA() == Incomparable()
iA() != Incomparable()
obaj powrótFalse
. Przy poprawnej implementacjiA.__ne__
(takiej, która zwraca,NotImplemented
gdy nie wie, jak zrobić porównanie), relacja jest symetryczna;A() != Incomparable()
iIncomparable() != A()
uzgodnić wynik (bo w pierwszym przypadkuA.__ne__
wracaNotImplemented
, potemIncomparable.__ne__
wracaFalse
, aw drugimIncomparable.__ne__
zwracaFalse
bezpośrednio). Ale kiedyA.__ne__
jest implementowane jakoreturn not self == other
,A() != Incomparable()
zwracaTrue
(ponieważA.__eq__
zwraca, nieNotImplemented
, potemIncomparable.__eq__
zwracaFalse
iA.__ne__
odwraca to doTrue
), podczas gdyIncomparable() != A()
zwracaFalse.
Możesz zobaczyć przykład tego w akcji tutaj .
Oczywiście klasa, która zawsze wraca
False
dla obu__eq__
i__ne__
jest trochę dziwna. Ale jak wspomniano wcześniej,__eq__
i__ne__
nawet nie trzeba zwracaćTrue
/False
; SQLAlchemy ORM ma klasy z komparatorami, które zwracają specjalny obiekt proxy do budowania zapytań, wcaleTrue
/False
wcale (są „prawdziwe”, jeśli są oceniane w kontekście logicznym, ale nigdy nie powinny być oceniane w takim kontekście).Poprzez brak przeciążenia
__ne__
prawidłowo, to będzie przerwa klas tego rodzaju, jak kod:zadziała (zakładając, że SQLAlchemy w ogóle wie, jak wstawić
MyClassWithBadNE
do ciągu SQL; można to zrobić za pomocą adapterów typów bezMyClassWithBadNE
konieczności jakiejkolwiek współpracy), przekazując oczekiwany obiekt proxy dofilter
, podczas gdy:zakończy się przekazaniem
filter
zwykłegoFalse
, ponieważself == other
zwraca obiekt proxy inot self == other
po prostu konwertuje prawdziwy obiekt proxy naFalse
. Miejmy nadzieję, żefilter
zgłasza wyjątek w przypadku obsługi nieprawidłowych argumentów, takich jakFalse
. Chociaż jestem pewien, że wielu będzie argumentować, żeMyTable.fieldname
powinno to być konsekwentnie po lewej stronie porównania, faktem jest, że nie ma programowego powodu, aby wymuszać to w ogólnym przypadku, a poprawny rodzaj generyczny__ne__
będzie działał tak czy inaczej, podczas gdyreturn not self == other
działa tylko w jednym układzie.źródło
Incomparable
klasy od tej klasie łamie dopełniacza relacji pomiędzy!=
i==
operatorów, a zatem może być uznane za nieważne lub „patologiczne” przykład jak @AaronHall umieścić go. I przyznaję, że @AaronHall ma rację, kiedy wskazał, że twój argument SQLAlchemy może zostać uznany za nieistotny, ponieważ jest w kontekście innym niż boolowski. (Twoje argumenty są nadal bardzo interesujące i przemyślane.)Prawidłowa
__ne__
realizacjaImplementacja metody specjalnej @ ShadowRanger
__ne__
jest poprawna:def __ne__(self, other): result = self.__eq__(other) if result is not NotImplemented: return not result return NotImplemented
Tak się składa, że jest to również domyślna implementacja metody specjalnej
__ne__
od czasu Pythona 3.4 , jak podano w dokumentacji Pythona :Należy również zauważyć, że zwracanie wartości
NotImplemented
nieobsługiwanych operandów nie jest specyficzne dla metody specjalnej__ne__
. W rzeczywistości wszystkie specjalne metody porównania 1 i specjalne metody numeryczne 2 powinny zwracać wartośćNotImplemented
dla nieobsługiwanych operandów , jak określono w dokumentacji Pythona :Przykład specjalnych metod numerycznych znajduje się w dokumentacji Pythona :
class MyIntegral(Integral): def __add__(self, other): if isinstance(other, MyIntegral): return do_my_adding_stuff(self, other) elif isinstance(other, OtherTypeIKnowAbout): return do_my_other_adding_stuff(self, other) else: return NotImplemented def __radd__(self, other): if isinstance(other, MyIntegral): return do_my_adding_stuff(other, self) elif isinstance(other, OtherTypeIKnowAbout): return do_my_other_adding_stuff(other, self) elif isinstance(other, Integral): return int(other) + int(self) elif isinstance(other, Real): return float(other) + float(self) elif isinstance(other, Complex): return complex(other) + complex(self) else: return NotImplemented
1 Specjalne metody porównania:
__lt__
,__le__
,__eq__
,__ne__
,__gt__
i__ge__
.2 specjalne metody numerycznej
__add__
,__sub__
,__mul__
,__matmul__
,__truediv__
,__floordiv__
,__mod__
,__divmod__
,__pow__
,__lshift__
,__rshift__
,__and__
,__xor__
,__or__
i ich__r*__
odbicie i__i*__
odpowiedników w miejscu.Nieprawidłowa
__ne__
implementacja # 1Implementacja metody specjalnej @ Falmarri
__ne__
jest nieprawidłowa:def __ne__(self, other): return not self.__eq__(other)
Problem z tą implementacją polega na tym, że nie opiera się ona na specjalnej metodzie
__ne__
innego operandu, ponieważ nigdy nie zwraca wartościNotImplemented
(wyrażenienot self.__eq__(other)
oblicza wartośćTrue
lubFalse
, w tym gdy jego podwyrażenie szacujeself.__eq__(other)
wartość,NotImplemented
ponieważ wyrażeniebool(NotImplemented)
oblicza wartośćTrue
). Boolowska ocena wartościNotImplemented
przerywa zależność dopełniania między operatorami porównania!=
i==
:class Correct: def __ne__(self, other): result = self.__eq__(other) if result is not NotImplemented: return not result return NotImplemented class Incorrect: def __ne__(self, other): return not self.__eq__(other) x, y = Correct(), Correct() assert (x != y) is not (x == y) x, y = Incorrect(), Incorrect() assert (x != y) is not (x == y) # AssertionError
Błędny
__ne__
implementacja # 2Implementacja metody specjalnej @ AaronHall
__ne__
jest również niepoprawna:def __ne__(self, other): return not self == other
Problem z tą implementacją polega na tym, że bezpośrednio powraca do specjalnej metody
__eq__
innego operandu, pomijając specjalną metodę__ne__
innego operandu, ponieważ nigdy nie zwraca wartościNotImplemented
(wyrażenienot self == other
powraca do specjalnej metody__eq__
innego operandu i zwraca wartośćTrue
lubFalse
). Pomijanie metody jest niepoprawne, ponieważ ta metoda może mieć skutki uboczne, takie jak aktualizowanie stanu obiektu:class Correct: def __init__(self): self.counter = 0 def __ne__(self, other): self.counter += 1 result = self.__eq__(other) if result is not NotImplemented: return not result return NotImplemented class Incorrect: def __init__(self): self.counter = 0 def __ne__(self, other): self.counter += 1 return not self == other x, y = Correct(), Correct() assert x != y assert x.counter == y.counter x, y = Incorrect(), Incorrect() assert x != y assert x.counter == y.counter # AssertionError
Zrozumienie operacji porównawczych
W matematyce relacja binarna R nad zbiorem X jest zbiorem uporządkowanych par ( x , y ) w X 2 . Instrukcja ( x , y ) w R brzmi „ x jest R- powiązane z y ” i jest oznaczona przez xRy .
Własności relacji binarnej R na zbiorze X :
Na przykład =. Jednak ≠ jest tylko symetryczne.
Na przykład ≤ i ≥.
Na przykład <i>. Jednak ≠ jest tylko nierefleksyjne.
Operacje na dwóch relacjach binarnych R i S na zbiorze X :
Relacje między relacjami porównawczymi, które są zawsze aktualne:
Relacje między relacjami porównawczymi, które są ważne tylko dla zamówień connex :
Tak, aby prawidłowo realizować w Pythonie operatory porównania
==
,!=
,<
,>
,<=
, i>=
odpowiadającej relacji porównania =, ≠, <,>, ≤, ≥ i wszystkie powyższe właściwości matematyczne i relacje powinny utrzymać.Operacja porównania
x operator y
wywołuje specjalną metodę porównania__operator__
klasy jednego z jej operandów:class X: def __operator__(self, other): # implementation
Ponieważ R jest zwrotna oznacza XRX , refleksyjnym operacja porównania
x operator y
(x == y
,x <= y
ix >= y
) lub refleksyjne specjalny wywołanie metody porównaniax.__operator__(y)
(x.__eq__(y)
,x.__le__(y)
ix.__ge__(y)
) należy oceniać wartościTrue
, jeślix
iy
są identyczne, to znaczy, jeśli wyrażeniex is y
ma wartośćTrue
. Ponieważ R jest irreflexive oznacza nie XRX , irreflexive operacja porównaniax operator y
(x != y
,x < y
ix > y
) lub irreflexive wywołanie specjalnej metody porównaniax.__operator__(y)
(x.__ne__(y)
,x.__lt__(y)
ax.__gt__(y)
) należy oceniać wartościFalse
jeślix
iy
są identyczne, to znaczy, jeślix is y
wynikiem wyrażenia jestTrue
. Nieruchomość odruchowy jest uważany przez Pythonie dla operatora porównania==
i związany specjalną metodę porównawczą__eq__
, ale zaskakująco nie uznane za operatorów porównania<=
i>=
i związany specjalnych metod porównawczych__le__
i__ge__
, a właściwość irreflexive jest uważany przez Pythonie dla operatora porównania!=
i związany specjalną metodę porównawczą__ne__
, ale zaskakująco nie uznane za operatorów porównania<
i>
i związany specjalnych metod porównawczych__lt__
i__gt__
. Zignorowane operatory porównania zamiast tego zgłaszają wyjątekTypeError
(i powiązane specjalne metody porównawcze zamiast tego zwracają wartośćNotImplemented
), jak wyjaśniono w dokumentacji Pythona :Klasa
object
zapewnia domyślne implementacje specjalnych metod porównania, które są dziedziczone przez wszystkie jej podklasy, jak wyjaśniono w dokumentacji Pythona :Od R = ( R T ) T , porównanie xRy jest równoważna odwrotnego stosunku yr T x (nieformalnie nazwie "odbicie" w dokumentacji Python). Istnieją więc dwa sposoby obliczenia wyniku operacji porównania
x operator y
: wywołanie albox.__operator__(y)
luby.__operatorT__(x)
. Python używa następującej strategii obliczeniowej:x.__operator__(y)
chyba że klasa prawego operandu jest potomkiem klasy lewego operandu, w którym to przypadku wywołujey.__operatorT__(x)
( pozwalając klasom na przesłonięcie odwrotnej specjalnej metody porównania ich przodków ).x
iy
nie są obsługiwane (wskazywane przez wartość zwracanąNotImplemented
), wywołuje specjalną metodę porównania converse jako pierwszą rezerwę .x
iy
są nieobsługiwane (wskazywane przez wartość zwracanąNotImplemented
), zgłasza wyjątekTypeError
z wyjątkiem operatorów porównania==
i!=
dla których testuje odpowiednio tożsamość i nie-tożsamość operandówx
orazy
jako drugą rezerwę (wykorzystując właściwość zwrotności==
i właściwość nieodwracalności!=
).W CPython ten wprowadzany jest kod C , który może ulegać translacji do Pythonie (nazwami
eq
dla==
,ne
na!=
,lt
o<
,gt
o>
,le
o<=
, age
w>=
)def eq(left, right): if type(left) != type(right) and 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 ne(left, right): if type(left) != type(right) and 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 lt(left, right): if type(left) != type(right) and 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__}' " f"and '{type(right).__name__}'" ) return result
def gt(left, right): if type(left) != type(right) and 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__}' " f"and '{type(right).__name__}'" ) return result
def le(left, right): if type(left) != type(right) and 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__}' " f"and '{type(right).__name__}'" ) return result
def ge(left, right): if type(left) != type(right) and 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__}' " f"and '{type(right).__name__}'" ) return result
Ponieważ R = ¬ (¬ R ), porównanie xRy jest równoważne porównaniu dopełniacza ¬ ( x ¬ Ry ). ≠ jest uzupełnieniem =, więc metoda specjalna
__ne__
jest implementowana__eq__
domyślnie w postaci specjalnej metody dla obsługiwanych operandów, podczas gdy inne specjalne metody porównania są domyślnie implementowane niezależnie (fakt, że ≤ jest sumą <i = oraz ≥ jest sumą> i = jest zaskakująco nieuwzględniona , co oznacza, że obecnie specjalne metody__le__
i__ge__
powinny być zaimplementowane przez użytkownika), jak wyjaśniono w dokumentacji Pythona :W CPythonie jest to zaimplementowane w kodzie C , który można przetłumaczyć na kod Pythona:
def __eq__(self, other): return self is other or 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
Więc domyślnie:
x operator y
podnosi wyjątekTypeError
z wyjątkiem operatorów porównania==
i!=
do którego powraca odpowiednio tożsamości i braku tożsamości argumentówx
iy
;x.__operator__(y)
zwraca wartośćNotImplemented
z wyjątkiem specjalnych metod porównawczych__eq__
i__ne__
dla których zwraca odpowiednioTrue
iFalse
jeśli operandyx
iy
są odpowiednio identyczne i nieidentyczne, a wartość wNotImplemented
przeciwnym razie.źródło
__ne__
metody, gdy__eq__
metoda zwraca NotImplemented, jest ona niepoprawna”. -A
definiuje bezwarunkową równość. ZatemA() == B()
. ZatemA() != B()
powinno być fałszem i tak jest . Podane przykłady są patologiczne (tj.__ne__
Nie powinny zwracać łańcucha i__eq__
nie powinny zależeć__ne__
- raczej__ne__
powinny zależeć od__eq__
, co jest domyślnym oczekiwaniem w Pythonie 3). Nadal jestem -1 w tej odpowiedzi, dopóki nie zmienisz zdania.NotImplemented
jeśli nie implementuje operacji dla danej pary argumentów. Zgodnie z konwencjąFalse
iTrue
są zwracane w celu pomyślnego porównania. Jednak metody te mogą zwracać dowolną wartość , więc jeśli operator porównania jest używany w kontekście boolowskim (np. w warunku instrukcji if), Python wywołabool()
wartość, aby określić, czy wynik jest prawdziwy, czy fałszywy. "B
które zwracają prawdziwy ciąg znaków przy wszystkich sprawdzeniach__ne__
iA
zwracająTrue
przy wszystkich sprawdzeniach__eq__
. To jest patologiczna sprzeczność. W przypadku takiej sprzeczności najlepiej byłoby zgłosić wyjątek. Bez wiedzyB
,A
nie ma obowiązku przestrzeganiaB
„s wdrażania__ne__
dla celów symetrii. W tym momencie w przykładzie nie ma dla mnie znaczenia sposób działaniaA
narzędzi__ne__
. Znajdź praktyczny, niepatologiczny przypadek, aby przedstawić swój punkt widzenia. Zaktualizowałem swoją odpowiedź, aby się do Ciebie zwrócić.Jeśli to wszystko
__eq__
,__ne__
,__lt__
,__ge__
,__le__
, i__gt__
sensu dla tej klasy, a potem po prostu wdrożyć__cmp__
zamiast. W przeciwnym razie rób to, co robisz, z powodu fragmentu, który powiedział Daniel DiPaolo (podczas gdy ja to testowałem zamiast sprawdzać;))źródło
__cmp__()
Specjalna metoda nie jest już obsługiwana w Pythonie 3.x więc powinniśmy się przyzwyczaić do korzystania z bogatych operatorów porównania.