Python, dovrei implementare un __ne__()
operatore basato su __eq__
?
Risposta breve: non implementarlo, ma se devi, usa ==
, no__eq__
In Python 3, !=
è la negazione di ==
default, quindi non ti viene nemmeno richiesto di scrivere a __ne__
, e la documentazione non è più supponente di scriverne uno.
In generale, per il codice Python 3 solo, non scriverne uno a meno che non sia necessario oscurare l'implementazione genitore, ad esempio per un oggetto incorporato.
Cioè, tieni presente il commento di Raymond Hettinger :
Il __ne__
metodo segue automaticamente __eq__
solo se
__ne__
non è già definito in una superclasse. Quindi, se stai ereditando da un builtin, è meglio sovrascriverli entrambi.
Se hai bisogno che il tuo codice funzioni in Python 2, segui i consigli per Python 2 e funzionerà perfettamente in Python 3.
In Python 2, Python stesso non implementa automaticamente alcuna operazione in termini di un'altra, quindi dovresti definire il __ne__
in termini di ==
invece di __eq__
. PER ESEMPIO
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)`
Vedi la prova
__ne__()
operatore di implementazione basato su __eq__
e
- non implementato affatto
__ne__
in Python 2
fornisce un comportamento non corretto nella dimostrazione di seguito.
Risposta lunga
La documentazione per Python 2 dice:
Non ci sono relazioni implicite tra gli operatori di confronto. La verità di x==y
non implica che x!=y
sia falso. Di conseguenza, quando si definisce __eq__()
, si dovrebbe anche definire in __ne__()
modo che gli operatori si comportino come previsto.
Quindi ciò significa che se definiamo __ne__
in termini di inverso di __eq__
, possiamo ottenere un comportamento coerente.
Questa sezione della documentazione è stata aggiornata per Python 3:
Per impostazione predefinita, __ne__()
delega __eq__()
e inverte il risultato a meno che non lo sia NotImplemented
.
e nella sezione "novità" , vediamo che questo comportamento è cambiato:
!=
ora restituisce l'opposto di ==
, a meno che non ==
ritorni NotImplemented
.
Per l'implementazione __ne__
, preferiamo utilizzare l' ==
operatore invece di utilizzare __eq__
direttamente il metodo in modo che se self.__eq__(other)
di una sottoclasse restituisce NotImplemented
il tipo verificato, Python controllerà opportunamente other.__eq__(self)
Dalla documentazione :
L' NotImplemented
oggetto
Questo tipo ha un unico valore. C'è un singolo oggetto con questo valore. A questo oggetto si accede tramite il nome predefinito
NotImplemented
. I metodi numerici e i metodi di confronto avanzato possono restituire questo valore se non implementano l'operazione per gli operandi forniti. (L'interprete proverà quindi l'operazione riflessa, o qualche altro fallback, a seconda dell'operatore.) Il suo valore di verità è vero.
Quando somministrato un ricco operatore di confronto, se non sono dello stesso tipo, controlla se il Python other
è un sottotipo, e se ha tale operatore definito, si utilizza il other
metodo di 's primo (inversa per <
, <=
, >=
e >
). Se NotImplemented
viene restituito, quindi utilizza il metodo del contrario. (Esso non controllare lo stesso metodo per due volte.) Uso del ==
operatore consente questa logica avvenire.
Aspettative
Semanticamente, dovresti implementare __ne__
in termini di controllo dell'uguaglianza perché gli utenti della tua classe si aspetteranno che le seguenti funzioni siano equivalenti per tutte le istanze di 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
Cioè, entrambe le funzioni precedenti dovrebbero sempre restituire lo stesso risultato. Ma questo dipende dal programmatore.
Dimostrazione di comportamento imprevisto durante la definizione __ne__
basata su __eq__
:
Prima la configurazione:
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."""
Crea istanze non equivalenti:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Comportamento atteso:
(Nota: mentre ogni seconda asserzione di ciascuna delle seguenti è equivalente e quindi logicamente ridondante a quella precedente, le includo per dimostrare che l' ordine non ha importanza quando una è una sottoclasse dell'altra. )
Queste istanze sono state __ne__
implementate con ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Anche queste istanze, testate con Python 3, funzionano correttamente:
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
E ricorda che questi sono stati __ne__
implementati con __eq__
- sebbene questo sia il comportamento previsto, l'implementazione non è corretta:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Comportamento inaspettato:
Si noti che questo confronto contraddice i confronti sopra ( not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
e,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Non saltare __ne__
in Python 2
Per provare che non dovresti saltare l'implementazione __ne__
in Python 2, vedi questi oggetti equivalenti:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
Il risultato sopra dovrebbe essere False
!
Sorgente Python 3
L'implementazione predefinita di CPython per __ne__
è typeobject.c
inobject_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;
Ma l'impostazione predefinita __ne__
utilizza __eq__
?
I __ne__
dettagli di implementazione predefiniti di Python 3 a livello C vengono usati __eq__
perché il livello più alto ==
( PyObject_RichCompare ) sarebbe meno efficiente e quindi deve anche gestire NotImplemented
.
Se __eq__
è implementato correttamente, anche la negazione di ==
è corretta e ci consente di evitare dettagli di implementazione di basso livello nel nostro __ne__
.
Utilizzando ==
ci permette di mantenere la nostra logica di basso livello in un luogo, ed evitare di affrontare NotImplemented
in __ne__
.
Si potrebbe erroneamente presumere che ==
possa tornare NotImplemented
.
In realtà utilizza la stessa logica dell'implementazione predefinita di __eq__
, che controlla l'identità (vedere do_richcompare e le nostre prove di seguito)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
E i confronti:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
Prestazione
Non credermi sulla parola, vediamo cosa è più performante:
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
Penso che questi numeri di prestazioni parlino da soli:
>>> 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
Questo ha senso se si considera che low_level_python
sta facendo logica in Python che altrimenti verrebbe gestita a livello C.
Risposta ad alcuni critici
Un altro risponditore scrive:
L'implementazione not self == other
di Aaron Hall del __ne__
metodo non è corretta in quanto non può mai restituire NotImplemented
( not NotImplemented
è False
) e quindi il __ne__
metodo che ha la priorità non può mai ripiegare sul __ne__
metodo che non ha la priorità.
Non essere __ne__
mai tornato NotImplemented
non lo rende errato. Invece, gestiamo la priorità con NotImplemented
tramite il controllo dell'uguaglianza con ==
. Supponendo che ==
sia implementato correttamente, abbiamo finito.
not self == other
era l'implementazione predefinita di Python 3 del __ne__
metodo, ma era un bug ed è stato corretto in Python 3.4 a gennaio 2015, come notato da ShadowRanger (vedi problema # 21408).
Bene, spieghiamo questo.
Come notato in precedenza, Python 3 gestisce per impostazione predefinita __ne__
controllando prima se self.__eq__(other)
restituisce NotImplemented
(un singleton), che dovrebbe essere controllato con is
e restituito in caso affermativo, altrimenti dovrebbe restituire l'inverso. Ecco quella logica scritta come un mix di classi:
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
Ciò è necessario per la correttezza dell'API Python di livello C ed è stato introdotto in Python 3, rendendo
ridondante. Tutti i __ne__
metodi pertinenti sono stati rimossi, compresi quelli che implementano il proprio controllo e quelli che delegano __eq__
direttamente o tramite ==
- ed ==
era il modo più comune per farlo.
La simmetria è importante?
Nostro critico persistente fornisce un esempio patologico per fare il caso per la gestione NotImplemented
in __ne__
, valorizzando simmetria soprattutto. Facciamo Steel-Man l'argomento con un chiaro esempio:
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)
Quindi, con questa logica, per mantenere la simmetria, dobbiamo scrivere il complicato __ne__
, indipendentemente dalla versione di Python.
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)
Apparentemente non dovremmo tenere conto del fatto che questi casi sono uguali e non uguali.
Propongo che la simmetria sia meno importante della presunzione di codice ragionevole e seguendo i consigli della documentazione.
Tuttavia, se A avesse un'implementazione ragionevole di __eq__
, allora potremmo ancora seguire la mia direzione qui e avremmo ancora simmetria:
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)
Conclusione
Per il codice compatibile con Python 2, usa ==
per implementare __ne__
. È di più:
- corretta
- semplice
- performante
Solo in Python 3, usa la negazione di basso livello sul livello C: è ancora più semplice e performante (sebbene il programmatore sia responsabile di determinare che sia corretto ).
Ancora una volta, non scrivere logica di basso livello in Python di alto livello.
__ne__
uso__eq__
, ma solo a implementarlo.