example - python class
Python, ¿debería implementar el operador__ne__() basado en__eq__? (5)
Tengo una clase donde quiero anular el __eq__() . Parece tener sentido que también deba anular al __ne__() , pero ¿tiene sentido implementar __ne__ basado en __eq__ como tal?
class A:
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
¿O hay algo que me falta con la forma en que Python usa estos operadores que hace que esta no sea una buena idea?
Python, ¿debería implementar el
__ne__()basado en__eq__?
Respuesta corta: No. Use == lugar de __eq__
En Python 3 __ne__ != Es la negación de == por defecto, por lo que ni siquiera está obligado a escribir un __ne__ , y la documentación ya no es obstinada al escribir una. Pero ten en cuenta el comentario de Raymond Hettinger :
El método
__ne__sigue automáticamente desde__eq__solo si__ne__no está ya definido en una superclase. Por lo tanto, si está heredando de una versión integrada, es mejor anular ambas.
Además, 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, defina el __ne__ en términos de == 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
- implementando el
__ne__()basado en__eq__y - no implementando
__ne__en Python 2 en absoluto
proporciona un comportamiento incorrecto en la demostración a continuación.
Respuesta larga
La documentation para Python 2 dice:
No hay relaciones implícitas entre los operadores de comparación. La verdad de
x==yno implica quex!=ysea 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 del inverso 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 a__eq__()e invierte el resultado a menos que no se hayaNotImplemented.
y en la sección "qué hay de nuevo" , vemos que este comportamiento ha cambiado:
!=ahora devuelve el opuesto de==, a menos que==regreseNotImplemented.
Para implementar __ne__ , preferimos usar el operador == lugar de usar el método __eq__ directamente, de modo que si self.__eq__(other) de una subclase devuelve NotImplemented para el tipo marcado, Python verificará apropiadamente other.__eq__(self) From the documentación :
El objeto NotImplemented
Este tipo tiene un solo valor. Hay un solo objeto con este valor. Se accede a este objeto a través del nombre
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 provistos. (El intérprete intentará entonces la operación reflejada, o alguna otra alternativa, dependiendo del operador). Su valor de verdad es verdadero.
Cuando se les da un operador de comparación rico, si no son del mismo tipo, Python comprueba si el other es un subtipo, y si tiene ese operador definido, primero usa el método del other (inverso para < , <= , >= y > ). Si se devuelve NotImplemented , utiliza el método opuesto. (No verifica el mismo método dos veces). Usar el operador == permite que esta lógica tenga lugar.
Esperanzas de heredar
Semánticamente, debe implementar __ne__ en términos de verificación de igualdad porque los usuarios de su clase esperan 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, ya que Python no implementa automáticamente ninguna operación en términos de otro.
Demostración de comportamiento inesperado al definir __ne__ basado en __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."""
Crear 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 al anterior, los incluyo para demostrar que el orden no importa cuando uno es una subclase del otro ) .
Estas instancias se han implementado __ne__ con == :
>>> assert not right1 == right2
>>> assert not right2 == right1
>>> assert right1 != right2
>>> assert right2 != right1
Estas instancias, prueba bajo 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 __ne__ implementados con __eq__ - mientras que 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 saltes __ne__ en Python 2
Para evidencia de que no debe omitir la implementación de __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 está en typeobject.c :
case Py_NE:
/* By default, __ne__() delegates to __eq__() and inverts the result,
unless the latter returns NotImplemented. */
if (self->ob_type->tp_richcompare == NULL) {
res = Py_NotImplemented;
Py_INCREF(res);
break;
}
res = (*self->ob_type->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;
Respuesta corta: sí (pero lea la documentación para hacerlo bien)
Aunque interesante, la respuesta de Aaron Hall no es la forma correcta de implementar el método __ne__ , porque con la implementación not self == other , el método __ne__ del otro operando nunca se considera. Por el contrario, como se demuestra a continuación, la implementación predeterminada de Python 3 del método __ne__ de un operando se __ne__ en el método __ne__ del otro operando al devolver NotImplemented cuando su método NotImplemented devuelve NotImplemented . ShadowRanger dio la implementación correcta del método __ne__ :
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
Implementación de los operadores de comparación
La Referencia de lenguaje Python para Python 3 establece en su capítulo III el modelo de datos :
object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)Estos son los llamados métodos de "comparación enriquecida". La correspondencia entre los símbolos del operador y los nombres de los métodos es la siguiente:
x<yllamadasx.__lt__(y),x<=yllamadasx.__le__(y),x==yllamadasx.__eq__(y),x!=yllama ax.__ne__(y),x>yllamax.__gt__(y), yx>=yllamax.__ge__(y).Un método de comparación rico puede devolver el singleton
NotImplementedsi no implementa la operación para un par de argumentos dado.No hay versiones de argumentos intercambiados de estos métodos (para usar cuando el argumento de la izquierda no admite la operación pero sí el argumento de la derecha); más bien,
__lt__()y__gt__()son reflejo el uno del otro,__le__()y__ge__()son reflejo el uno del otro, y__eq__()y__ne__()son su propio reflejo. Si los operandos son de tipos diferentes, y el tipo de operando derecho es una subclase directa o indirecta del tipo del operando izquierdo, el método reflejado del operando derecho tiene prioridad, de lo contrario, el método del operando izquierdo tiene prioridad. La subclasificación virtual no se considera.
Traduciendo esto al código de Python ( operator_eq para == , operator_ne for != , operator_lt para < , operator_gt for > , operator_le para <= y operator_ge para >= ):
def operator_eq(left, right):
if isinstance(right, type(left)):
result = right.__eq__(left)
if result is NotImplemented:
result = left.__eq__(right)
else:
result = left.__eq__(right)
if result is NotImplemented:
result = right.__eq__(left)
if result is NotImplemented:
result = left is right
return result
def operator_ne(left, right):
if isinstance(right, type(left)):
result = right.__ne__(left)
if result is NotImplemented:
result = left.__ne__(right)
else:
result = left.__ne__(right)
if result is NotImplemented:
result = right.__ne__(left)
if result is NotImplemented:
result = left is not right
return result
def operator_lt(left, right):
if isinstance(right, type(left)):
result = right.__gt__(left)
if result is NotImplemented:
result = left.__lt__(right)
else:
result = left.__lt__(right)
if result is NotImplemented:
result = right.__gt__(left)
if result is NotImplemented:
raise TypeError(f"''<'' not supported between instances of ''{type(left).__name__}'' and ''{type(right).__name__}''")
return result
def operator_gt(left, right):
if isinstance(right, type(left)):
result = right.__lt__(left)
if result is NotImplemented:
result = left.__gt__(right)
else:
result = left.__gt__(right)
if result is NotImplemented:
result = right.__lt__(left)
if result is NotImplemented:
raise TypeError(f"''>'' not supported between instances of ''{type(left).__name__}'' and ''{type(right).__name__}''")
return result
def operator_le(left, right):
if isinstance(right, type(left)):
result = right.__ge__(left)
if result is NotImplemented:
result = left.__le__(right)
else:
result = left.__le__(right)
if result is NotImplemented:
result = right.__ge__(left)
if result is NotImplemented:
raise TypeError(f"''<='' not supported between instances of ''{type(left).__name__}'' and ''{type(right).__name__}''")
return result
def operator_ge(left, right):
if isinstance(right, type(left)):
result = right.__le__(left)
if result is NotImplemented:
result = left.__ge__(right)
else:
result = left.__ge__(right)
if result is NotImplemented:
result = right.__le__(left)
if result is NotImplemented:
raise TypeError(f"''>='' not supported between instances of ''{type(left).__name__}'' and ''{type(right).__name__}''")
return result
Implementación por defecto de los métodos de comparación
La documentación agrega:
De forma predeterminada,
__ne__()delega a__eq__()e invierte el resultado a menos que no se hayaNotImplemented. No hay otras relaciones implícitas entre los operadores de comparación, por ejemplo, la verdad de(x<y or x==y)no implicax<=y.
La implementación predeterminada de los métodos de comparación ( __eq__ , __ne__ , __lt__ , __gt__ , __le__ y __ge__ ) puede ser dada por:
def __eq__(self, other):
return NotImplemented
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
def __lt__(self, other):
return NotImplemented
def __gt__(self, other):
return NotImplemented
def __le__(self, other):
return NotImplemented
def __ge__(self, other):
return NotImplemented
Entonces esta es la implementación correcta del método __ne__ . Y no siempre devuelve el inverso del método __eq__ porque cuando el método __eq__ devuelve NotImplemented , su valor inverso not NotImplemented es False (como bool(NotImplemented) es True ) en lugar del NotImplemented deseado.
Implementaciones incorrectas de __ne__
Como demostró anteriormente Aaron Hall, not self.__eq__(other) no es la implementación correcta del método __ne__ . Pero tampoco es not self == other . Esto último se demuestra a continuación comparando el comportamiento de la implementación predeterminada con el comportamiento de la implementación not self == other en dos casos:
- el método
NotImplementeddevuelveNotImplemented; - el método
__eq__devuelve un valor diferente deNotImplemented.
Implementación predeterminada
Veamos qué sucede cuando el método A.__ne__ usa la implementación predeterminada y el método A.__eq__ devuelve NotImplemented :
class A:
pass
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) == "B.__ne__"
-
!=llamaA.__ne__. -
A.__ne__llamaA.__eq__. -
A.__eq__devuelveNotImplemented. -
!=llamaB.__ne__. -
B.__ne__devuelve"B.__ne__".
Esto muestra que cuando el método A.__eq__ devuelve NotImplemented , el método A.__ne__ método B.__ne__ .
Ahora veamos qué sucede cuando el método A.__ne__ usa la implementación predeterminada y el método A.__eq__ devuelve un valor diferente de NotImplemented :
class A:
def __eq__(self, other):
return True
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is False
-
!=llamaA.__ne__. -
A.__ne__llamaA.__eq__. -
A.__eq__devuelveTrue. -
!=devuelvenot True, eso esFalse.
Esto muestra que en este caso, el método A.__ne__ devuelve el inverso del método A.__eq__ . Por lo tanto, el método __ne__ se comporta como se anuncia en la documentación.
Anulando la implementación predeterminada del método A.__ne__ con la implementación correcta dada anteriormente, se obtienen los mismos resultados.
not self == other implementación
Veamos qué sucede cuando se reemplaza la implementación predeterminada del método A.__ne__ con la implementación not self == other y el método A.__eq__ devuelve NotImplemented :
class A:
def __ne__(self, other):
return not self == other
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is True
-
!=llamaA.__ne__. -
A.__ne__calls==. -
==llamaA.__eq__. -
A.__eq__devuelveNotImplemented. -
==llama aB.__eq__. -
B.__eq__devuelveNotImplemented. -
==devuelveA() is B(), eso esFalse. -
A.__ne__not False, eso esTrue.
La implementación predeterminada del método __ne__ devolvió "B.__ne__" , no True .
Ahora veamos qué sucede al anular la implementación predeterminada del método A.__ne__ con la implementación not self == other y el método A.__eq__ devuelve un valor diferente de NotImplemented :
class A:
def __eq__(self, other):
return True
def __ne__(self, other):
return not self == other
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is False
-
!=llamaA.__ne__. -
A.__ne__calls==. -
==llamaA.__eq__. -
A.__eq__devuelveTrue. -
A.__ne__not True, eso esFalse.
La implementación predeterminada del método __ne__ también devolvió False en este caso.
Como esta implementación no puede replicar el comportamiento de la implementación predeterminada del método __ne__ cuando el método NotImplemented devuelve NotImplemented , es incorrecto.
Sí, está perfectamente bien. De hecho, la documentación lo insta a definir __ne__ cuando define __eq__ :
No hay relaciones implícitas entre los operadores de comparación. La verdad de
x==yno implica quex!=ysea falso. En consecuencia, al definir__eq__(), también se debe definir__ne__()para que los operadores se comporten como se espera.
En muchos casos (como este), será tan simple como negar el resultado de __eq__ , pero no siempre.
Si todos los __eq__ , __ne__ , __lt__ , __ge__ , __le__ y __gt__ tienen sentido para la clase, entonces simplemente implemente __cmp__ en __cmp__ lugar. De lo contrario, haz lo que estás haciendo, debido a lo poco que dijo Daniel DiPaolo (mientras lo estaba probando en lugar de buscarlo;))
Solo para el registro, un __ne__ portatil __ne__ / Py3 canonicamente correcto y cruzado se vería así:
import sys
class ...:
...
def __eq__(self, other):
...
if sys.version_info[0] == 2:
def __ne__(self, other):
equal = self.__eq__(other)
return equal if equal is NotImplemented else not equal
Esto funciona con cualquier __eq__ que pueda definir y, a diferencia de not (self == other) , no interfiere en algunos casos molestos / complejos que implican comparaciones entre instancias donde una instancia es de una subclase de la otra. Si su __eq__ no utiliza los retornos NotImplemented , esto funciona (con sobrecarga sin sentido), si utiliza NotImplemented veces, esto lo maneja correctamente. Y la comprobación de la versión de Python significa que si la clase se import en Python 3, __ne__ sin definir, lo que permitirá que la implementación __ne__ y eficiente de __ne__ (una versión C de lo anterior) se haga cargo.