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==ynon implica che x!=ysia 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 NotImplementedil tipo verificato, Python controllerà opportunamente other.__eq__(self) Dalla documentazione :
L' NotImplementedoggetto
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 othermetodo di 's primo (inversa per <, <=, >=e >). Se NotImplementedviene 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.cinobject_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 NotImplementedin __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_pythonsta facendo logica in Python che altrimenti verrebbe gestita a livello C.
Risposta ad alcuni critici
Un altro risponditore scrive:
L'implementazione not self == otherdi 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 NotImplementednon lo rende errato. Invece, gestiamo la priorità con NotImplementedtramite il controllo dell'uguaglianza con ==. Supponendo che ==sia implementato correttamente, abbiamo finito.
not self == otherera 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 ise 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 NotImplementedin __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.