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ł?
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, żex!=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.
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, żex!=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 jestNotImplemented
.
I w "Co nowego" widzimy, że to zachowanie się zmieniło:
!=
teraz zwraca przeciwieństwo==
, chyba że==
zwracaNotImplemented
.
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
Python sprawdza, czyTen 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.
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.
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.
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ć;)) {]}
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łaniax.__lt__(y)
,x<=y
połączeniax.__le__(y)
,x==y
połączeniax.__eq__(y)
,x!=y
rozmowyx.__ne__(y)
,x>y
wywołaniax.__gt__(y)
ix>=y
połączeniax.__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 jestNotImplemented
. Nie ma innych domniemanych zależności między operatorami porównania, np. prawda z(x<y or x==y)
nie oznaczax<=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__
zwracaNotImplemented
; - 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__"
-
!=
połączeniaA.__ne__
. -
A.__ne__
połączeniaA.__eq__
. -
A.__eq__
zwracaNotImplemented
. -
!=
połączeniaB.__ne__
. -
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
-
!=
połączeniaA.__ne__
. -
A.__ne__
połączeniaA.__eq__
. -
A.__eq__
zwracaTrue
. -
!=
zwracanot True
, czyliFalse
.
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
-
!=
połączeniaA.__ne__
. -
A.__ne__
połączenia==
. -
==
połączeniaA.__eq__
. -
A.__eq__
zwracaNotImplemented
. -
==
połączeniaB.__eq__
. -
B.__eq__
zwracaNotImplemented
. -
==
zwracaA() is B()
, czyliFalse
. -
A.__ne__
zwracanot False
, czyliTrue
.
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
-
!=
połączeniaA.__ne__
. -
A.__ne__
połączenia==
. -
==
rozmowyA.__eq__
. -
A.__eq__
zwracaTrue
. -
A.__ne__
zwracanot True
, czyliFalse
.
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.
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