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==y
no implica quex!=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 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<y
llamadasx.__lt__(y)
,x<=y
llamadasx.__le__(y)
,x==y
llamadasx.__eq__(y)
,x!=y
llama ax.__ne__(y)
,x>y
llamax.__gt__(y)
, yx>=y
llamax.__ge__(y)
.Un método de comparación rico puede devolver el singleton
NotImplemented
si 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
NotImplemented
devuelveNotImplemented
; - 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==y
no implica quex!=y
sea 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.