Python, ¿debería implementar el __ne__()
operador basado en __eq__
?
Respuesta corta: No lo implemente, pero si debe hacerlo, use ==
, no__eq__
En Python 3, !=
es la negación de ==
por defecto, por lo que ni siquiera está obligado a escribir un __ne__
, y la documentación ya no tiene opiniones sobre escribir uno.
En términos generales, para el código solo de Python 3, no escriba uno a menos que necesite eclipsar la implementación principal, por ejemplo, para un objeto incorporado.
Es decir, tenga en cuenta el comentario de Raymond Hettinger :
El __ne__
método sigue automáticamente de __eq__
solo si aún
__ne__
no está definido en una superclase. Entonces, si está heredando de un incorporado, es mejor anular ambos.
Si necesita que su código funcione en Python 2, siga la recomendación para Python 2 y funcionará perfectamente en Python 3.
En Python 2, Python en sí no implementa automáticamente ninguna operación en términos de otra; por lo tanto, debe definir __ne__
en términos de en ==
lugar de __eq__
. P.EJ
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)`
Ver prueba de que
- implementador de
__ne__()
operador basado en__eq__
y
- no implementado
__ne__
en Python 2 en absoluto
proporciona un comportamiento incorrecto en la siguiente demostración.
Respuesta larga
La documentación de Python 2 dice:
No hay relaciones implícitas entre los operadores de comparación. La verdad de x==y
no implica que x!=y
sea falso. En consecuencia, al definir __eq__()
, también se debe definir __ne__()
para que los operadores se comporten como se espera.
Entonces eso significa que si definimos __ne__
en términos de la inversa de__eq__
, podemos obtener un comportamiento consistente.
Esta sección de la documentación se ha actualizado para Python 3:
De forma predeterminada, __ne__()
delega __eq__()
e invierte el resultado a menos que lo sea NotImplemented
.
y en la sección "novedades" , vemos que este comportamiento ha cambiado:
!=
ahora devuelve lo contrario de ==
, a menos que ==
devuelva NotImplemented
.
Para la implementación __ne__
, preferimos usar el ==
operador en lugar de usar el __eq__
método directamente, de modo que si self.__eq__(other)
una subclase devuelve NotImplemented
el tipo marcado, Python lo verificará adecuadamente en other.__eq__(self)
la documentación :
los NotImplemented
objeto
Este tipo tiene un solo valor. Hay un solo objeto con este valor. Se accede a este objeto a través del nombre integrado
NotImplemented
. Los métodos numéricos y los métodos de comparación enriquecidos pueden devolver este valor si no implementan la operación para los operandos proporcionados. (El intérprete intentará entonces la operación reflejada, o alguna otra alternativa, según el operador). Su valor de verdad es verdadero.
Cuando se le dé un operador de comparación rica, si no son del mismo tipo, comprueba si el Python other
es un subtipo, y si tiene que definido por el operador, se utiliza el other
método 's primero (inversa para <
, <=
, >=
y >
). Si NotImplemented
se devuelve, entonces usa el método opuesto. (No no solicitar el mismo método dos veces.) Utilizando el ==
operador permite esta lógica a tener lugar.
Expectativas
Semánticamente, debe implementar __ne__
en términos de verificación de igualdad porque los usuarios de su clase esperarán que las siguientes funciones sean equivalentes para todas las instancias de 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
Es decir, ambas funciones anteriores siempre deben devolver el mismo resultado. Pero esto depende del programador.
Demostración de comportamiento inesperado al definir en __ne__
base a __eq__
:
Primero la configuración:
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."""
Cree instancias no equivalentes:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Comportamiento esperado:
(Nota: si bien cada segunda afirmación de cada uno de los siguientes es equivalente y, por lo tanto, lógicamente redundante con el anterior, los incluyo para demostrar que el orden no importa cuando uno es una subclase del otro ) .
Estas instancias se han __ne__
implementado con ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Estas instancias, probadas en Python 3, también funcionan correctamente:
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
Y recuerde que estos se han __ne__
implementado con __eq__
- si bien este es el comportamiento esperado, la implementación es incorrecta:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Comportamiento inesperado:
Tenga en cuenta que esta comparación contradice las comparaciones anteriores ( not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
y,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
No te saltes __ne__
en Python 2
Para obtener evidencia de que no debe omitir la implementación __ne__
en Python 2, consulte estos objetos equivalentes:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
¡El resultado anterior debería ser False
!
Fuente de Python 3
La implementación de CPython predeterminada para __ne__
está typeobject.c
enobject_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;
¿Pero los __ne__
usos predeterminados __eq__
?
El __ne__
detalle de implementación predeterminado de Python 3 en el nivel C se usa __eq__
porque el nivel superior ==
( PyObject_RichCompare ) sería menos eficiente y, por lo tanto, también debe manejar NotImplemented
.
Si __eq__
se implementa correctamente, la negación de ==
también es correcta, y nos permite evitar detalles de implementación de bajo nivel en nuestro __ne__
.
Usando ==
nos permite mantener nuestro nivel lógico bajo en un lugar, y evitar hacer frente NotImplemented
en __ne__
.
Uno podría asumir incorrectamente que ==
puede volver NotImplemented
.
En realidad, utiliza la misma lógica que la implementación predeterminada de __eq__
, que verifica la identidad (consulte do_richcompare y nuestra evidencia a continuación)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
Y las comparaciones:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
Actuación
No confíe en mi palabra, veamos qué es más eficaz:
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
Creo que estos números de rendimiento hablan por sí mismos:
>>> 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
Esto tiene sentido si se considera que low_level_python
está haciendo lógica en Python que de otra manera se manejaría en el nivel C.
Respuesta a algunas críticas
Otro contestador escribe:
La implementación not self == other
de Aaron Hall del __ne__
método es incorrecta, ya que nunca puede regresar NotImplemented
( not NotImplemented
es False
) y, por lo tanto, el __ne__
método que tiene prioridad nunca puede recurrir al __ne__
método que no tiene prioridad.
No haber __ne__
vuelto nunca NotImplemented
no lo hace incorrecto. En cambio, manejamos la priorización con a NotImplemented
través de la verificación de igualdad con ==
. Suponiendo que ==
se implemente correctamente, hemos terminado.
not self == other
solía ser la implementación predeterminada de Python 3 del __ne__
método, pero era un error y se corrigió en Python 3.4 en enero de 2015, como lo notó ShadowRanger (consulte el número 21408).
Bueno, expliquemos esto.
Como se señaló anteriormente, Python 3 se maneja de forma predeterminada __ne__
verificando primero si self.__eq__(other)
devuelve NotImplemented
(un singleton), que debe verificarse is
y devolverse si es así, de lo contrario, debe devolver el inverso. Aquí está esa lógica escrita como una mezcla de clases:
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
Esto es necesario para la corrección de la API de Python de nivel C, y se introdujo en Python 3, haciendo
redundante. __ne__
Se eliminaron todos los métodos relevantes , incluidos los que implementan su propia verificación, así como los que delegan __eq__
directamente o vía ==
, y ==
fue la forma más común de hacerlo.
¿Es importante la simetría?
Nuestra persistente crítico proporciona un ejemplo patológico para hacer el caso para el manejo NotImplemented
de __ne__
, la valoración de la simetría por encima de todo. Vamos a hacer un hombre de acero el argumento con un ejemplo claro:
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)
Entonces, según esta lógica, para mantener la simetría, necesitamos escribir lo complicado __ne__
, independientemente de la versión de 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)
Aparentemente, no deberíamos prestar atención a que estos casos sean iguales y no iguales.
Propongo que la simetría es menos importante que la presunción de código sensato y siguiendo los consejos de la documentación.
Sin embargo, si A tuviera una implementación sensata de __eq__
, entonces aún podríamos seguir mi dirección aquí y aún tendríamos simetría:
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)
Conclusión
Para código compatible con Python 2, use ==
para implementar __ne__
. Es más:
- correcto
- sencillo
- performante
Solo en Python 3, use la negación de bajo nivel en el nivel C; es aún más simple y eficaz (aunque el programador es responsable de determinar que es correcto ).
Nuevamente, no escriba lógica de bajo nivel en Python de alto nivel.
__ne__
uso__eq__
, solo que lo implemente.