Python, czy powinienem zaimplementować __ne__()
operator oparty na __eq__
?
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 :
__ne__
Sposób następuje automatycznie __eq__
tylko wtedy, gdy
__ne__
nie jest już zdefiniowane w nadrzędnej. Jeśli więc dziedziczysz 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 implementuje automatycznie żadnej operacji w kategoriach innej - dlatego powinieneś zdefiniować __ne__
w kategoriach ==
zamiast __eq__
. NA PRZYKŁAD
class A(object):
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self == other
Zobacz dowód
__ne__()
operator wdrażający oparty na __eq__
i
- w ogóle nie implementuje
__ne__
w Pythonie 2
zapewnia nieprawidłowe zachowanie w poniższej demonstracji.
Długa odpowiedź
Dokumentacji dla Pythona 2 mówi:
Nie ma domniemanych relacji między operatorami porównania. Prawda x==y
nie oznacza, że x!=y
jest fałszywa. W związku z tym podczas definiowania __eq__()
należy również zdefiniować __ne__()
tak, aby operatorzy zachowywali się zgodnie z oczekiwaniami.
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:
Domyślnie __ne__()
deleguje __eq__()
i odwraca wynik, chyba że tak jest NotImplemented
.
aw sekcji „co nowego” widzimy, że zmieniło się to zachowanie:
!=
teraz zwraca przeciwieństwo ==
, chyba że ==
zwraca NotImplemented
.
Do implementacji __ne__
wolimy używać ==
operatora zamiast bezpośrednio używać __eq__
metody, więc jeśli self.__eq__(other)
podklasa zwróci NotImplemented
dla sprawdzonego typu, Python odpowiednio sprawdzi other.__eq__(self)
Z dokumentacji :
NotImplemented
przedmiot
Ten typ ma jedną wartość. Istnieje jeden obiekt o tej wartości. Dostęp do tego obiektu uzyskuje się za pośrednictwem wbudowanej nazwy
NotImplemented
. Metody numeryczne i bogate metody porównania mogą zwracać tę wartość, jeśli nie implementują operacji dla podanych operandów. (W zależności od operatora interpreter spróbuje wykonać odzwierciedloną operację lub inną rezerwę). Jego wartość prawda to prawda.
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żywa other
pierwszy „s metody (odwrotność do <
, <=
, >=
i >
). Jeśli NotImplemented
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
assert not wrong2 == wrong1
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 2
Aby 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
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:
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ąć adresowania NotImplemented
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:
Realizacja Aaron Hall not self == other
z __ne__
metody jest błędne, gdyż nigdy nie może wrócić NotImplemented
( not NotImplemented
jest False
), a zatem __ne__
metoda, która ma pierwszeństwo nigdy nie może spaść z powrotem na __ne__
metody, które nie mają priorytet.
Brak __ne__
powrotu NotImplemented
nie oznacza, że jest to błędne. Zamiast tego obsługujemy priorytetyzację za NotImplemented
pomocą sprawdzania równości z ==
. Zakładając, że ==
zostało poprawnie zaimplementowane, gotowe.
not self == other
była to domyślna implementacja __ne__
metody w Pythonie 3, ale był to błąd i został poprawiony w Pythonie 3.4 w styczniu 2015 r., jak zauważył ShadowRanger (patrz numer 21408).
Cóż, wyjaśnijmy to.
Jak wspomniano wcześniej, Python 3 domyślnie obsługuje __ne__
, najpierw sprawdzając, czy self.__eq__(other)
zwraca NotImplemented
(singleton) - co powinno być sprawdzane is
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
zbę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
>>> 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:
- poprawny
- prosty
- wykonujący
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.
__ne__
using__eq__
, a jedynie do jej wdrożenia.