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==ynie oznacza, że x!=yjest 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 NotImplementeddla sprawdzonego typu, Python odpowiednio sprawdzi other.__eq__(self) Z dokumentacji :
NotImplementedprzedmiot
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 otherjest podtypem, a jeśli ma to operator zdefiniowany, używa otherpierwszy „s metody (odwrotność do <, <=, >=i >). Jeśli NotImplementedjest 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.cwobject_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 NotImplementedw __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_pythonw 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 == otherz __ne__metody jest błędne, gdyż nigdy nie może wrócić NotImplemented( not NotImplementedjest 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 NotImplementednie oznacza, że jest to błędne. Zamiast tego obsługujemy priorytetyzację za NotImplementedpomocą sprawdzania równości z ==. Zakładając, że ==zostało poprawnie zaimplementowane, gotowe.
not self == otherbył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 isi 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 NotImplementedw __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.