Tengo una clase en la que quiero anular el __eq__()
operador. Parece tener sentido que debería reemplazar el __ne__()
operador así, pero ¿tiene sentido para 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 esto no sea una buena idea?
fuente
__ne__
uso__eq__
, solo que lo implemente.NotImplemented
retorno de un lado como una señal para delegar__ne__
en el otro lado,not self == other
(asumiendo que el operando__eq__
no sabe cómo comparar el otro operando) está delegando implícitamente__eq__
desde el otro lado y luego invirtiéndolo. Para tipos extraños, por ejemplo, los campos del ORM de SQLAlchemy, esto causa problemas .__ne__
delega automáticamente a__eq__
y la cita en esta respuesta ya no existe en los documentos. En pocas palabras, es perfectamente pitónico implementar__eq__
y dejar__ne__
delegar.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 :
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.EJVer prueba de que
__ne__()
operador basado en__eq__
y__ne__
en Python 2 en absolutoproporciona un comportamiento incorrecto en la siguiente demostración.
Respuesta larga
La documentación de Python 2 dice:
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:
y en la sección "novedades" , vemos que este comportamiento ha cambiado:
Para la implementación
__ne__
, preferimos usar el==
operador en lugar de usar el__eq__
método directamente, de modo que siself.__eq__(other)
una subclase devuelveNotImplemented
el tipo marcado, Python lo verificará adecuadamente enother.__eq__(self)
la documentación :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 elother
método 's primero (inversa para<
,<=
,>=
y>
). SiNotImplemented
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: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:
Cree instancias no equivalentes:
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==
:Estas instancias, probadas en Python 3, también funcionan correctamente:
Y recuerde que estos se han
__ne__
implementado con__eq__
- si bien este es el comportamiento esperado, la implementación es incorrecta:Comportamiento inesperado:
Tenga en cuenta que esta comparación contradice las comparaciones anteriores (
not wrong1 == wrong2
).y,
No te saltes
__ne__
en Python 2Para obtener evidencia de que no debe omitir la implementación
__ne__
en Python 2, consulte estos objetos equivalentes:¡El resultado anterior debería ser
False
!Fuente de Python 3
La implementación de CPython predeterminada para
__ne__
estátypeobject.c
enobject_richcompare
:¿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 manejarNotImplemented
.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 frenteNotImplemented
en__ne__
.Uno podría asumir incorrectamente que
==
puede volverNotImplemented
.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)Y las comparaciones:
Actuación
No confíe en mi palabra, veamos qué es más eficaz:
Creo que estos números de rendimiento hablan por sí mismos:
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:
No haber
__ne__
vuelto nuncaNotImplemented
no lo hace incorrecto. En cambio, manejamos la priorización con aNotImplemented
través de la verificación de igualdad con==
. Suponiendo que==
se implemente correctamente, hemos terminado.Bueno, expliquemos esto.
Como se señaló anteriormente, Python 3 se maneja de forma predeterminada
__ne__
verificando primero siself.__eq__(other)
devuelveNotImplemented
(un singleton), que debe verificarseis
y devolverse si es así, de lo contrario, debe devolver el inverso. Aquí está esa lógica escrita como una mezcla de clases:Esto es necesario para la corrección de la API de Python de nivel C, y se introdujo en Python 3, haciendo
__ne__
métodos de este parche para cerrar el problema 21408 y__ne__
métodos en la limpieza de seguimiento eliminados aquí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:Entonces, según esta lógica, para mantener la simetría, necesitamos escribir lo complicado
__ne__
, independientemente de la versión de Python.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:Conclusión
Para código compatible con Python 2, use
==
para implementar__ne__
. Es más: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.
fuente
a1 != c2
regresóFalse
--- no se ejecutóa1.__ne__
, peroc2.__ne__
, lo que negó el método de Mixin__eq__
. Ya queNotImplemented
es verdad,not NotImplemented
esFalse
.not (self == other)
, pero nadie discute que no es rápido (bueno, más rápido que cualquier otra opción en Py2 de todos modos). El problema es que está mal en algunos casos; Python solía hacerlonot (self == other)
, pero cambió porque era incorrecto en presencia de subclases arbitrarias . Lo más rápido para la respuesta incorrecta sigue siendo incorrecta .__ne__
delegados a__eq__
(de ambos lados si es necesario), pero nunca vuelve al__ne__
del otro lado incluso cuando ambos__eq__
"se rinden". Los__ne__
delegados correctos a los suyos__eq__
, pero si eso regresaNotImplemented
, retrocede para ir a los del otro lado__ne__
, en lugar de invertir los del otro lado__eq__
(ya que es posible que el otro lado no haya optado explícitamente por delegar en__eq__
, y usted no debería estar tomando esa decisión por ello).__eq__
ni__ne__
devuelve niTrue
oFalse
, sino un objeto proxy (que resulta ser "verdadero"). Implementar incorrectamente el__ne__
orden de los medios es importante para la comparación (solo obtiene un proxy en un pedido).__ne__
completo. Dentro de un año, Py2 estará muerto y lo ignoramos. :-)Solo para el registro, un portátil Py2 / Py3 canónicamente correcto y cruzado
__ne__
se vería así:Esto funciona con cualquiera
__eq__
que pueda definir:not (self == other)
, no interfiere en algunos casos molestos / complejos que involucran comparaciones donde una de las clases involucradas no implica que el resultado de__ne__
sea el mismo que el resultado denot
on__eq__
(por ejemplo, el ORM de SQLAlchemy, donde ambos__eq__
y__ne__
devuelven objetos proxy especiales, noTrue
oFalse
, y tratandonot
de__eq__
devolver el resultado deFalse
, en lugar del objeto proxy correcto).not self.__eq__(other)
, esto delega correctamente a la__ne__
de la otra instancia cuandoself.__eq__
retornaNotImplemented
(not self.__eq__(other)
sería extra incorrecto, porqueNotImplemented
es veraz, entonces cuando__eq__
no supiera cómo realizar la comparación,__ne__
regresaríaFalse
, lo que implica que los dos objetos eran iguales cuando en realidad el único objeto preguntado no tenía idea, lo que implicaría un valor predeterminado de no igual)Si
__eq__
no usaNotImplemented
devoluciones, esto funciona (con una sobrecarga sin sentido), si se usa aNotImplemented
veces, lo maneja correctamente. Y la verificación de la versión de Python significa que si la clase estáimport
-ed en Python 3,__ne__
se deja sin definir, lo que permite que la__ne__
implementación alternativa nativa y eficiente de Python (una versión C de la anterior) se haga cargo.Por que esto es necesario
Reglas de sobrecarga de Python
La explicación de por qué hace esto en lugar de otras soluciones es algo misteriosa. Python tiene un par de reglas generales sobre los operadores de sobrecarga y los operadores de comparación en particular:
LHS OP RHS
, intenteLHS.__op__(RHS)
, y si vuelveNotImplemented
, intenteRHS.__rop__(LHS)
. Excepción: siRHS
es una subclase deLHS
la clase de, pruebeRHS.__rop__(LHS)
primero . En el caso de los operadores de comparación,__eq__
y__ne__
son sus propios "rop" (por lo que el orden de prueba para__ne__
esLHS.__ne__(RHS)
, entoncesRHS.__ne__(LHS)
, invertido siRHS
es una subclase deLHS
la clase de)LHS.__eq__(RHS)
devolverTrue
no implicaLHS.__ne__(RHS)
retornosFalse
(de hecho, los operadores ni siquiera están obligados a devolver valores booleanos; los ORM como SQLAlchemy intencionalmente no lo hacen, lo que permite una sintaxis de consulta más expresiva). A partir de Python 3, la__ne__
implementación predeterminada se comporta de esta manera, pero no es contractual; puede anular__ne__
de formas que no sean estrictamente opuestas a__eq__
.Cómo se aplica esto a los comparadores de sobrecarga
Entonces, cuando sobrecarga a un operador, tiene dos trabajos:
NotImplemented
, para que Python pueda delegar en la implementación del otro operandoEl problema con
not self.__eq__(other)
nunca delega al otro lado (y es incorrecto si
__eq__
regresa correctamenteNotImplemented
). Cuandoself.__eq__(other)
regresaNotImplemented
(que es "veraz"), regresa silenciosamenteFalse
, entoncesA() != something_A_knows_nothing_about
regresaFalse
, cuando debería haber verificado sisomething_A_knows_nothing_about
sabía cómo comparar con instancias deA
, y si no lo hace, debería haber regresadoTrue
(ya que si ninguna de las partes sabe cómo en comparación con el otro, se consideran no iguales entre sí). SiA.__eq__
está implementado incorrectamente (regresando enFalse
lugar deNotImplemented
cuando no reconoce al otro lado), entonces esto es "correcto" desdeA
la perspectiva de, regresandoTrue
(desdeA
que no cree que sea igual, entonces no es igual), pero podría ser mal desomething_A_knows_nothing_about
la perspectiva, ya que ni siquiera preguntósomething_A_knows_nothing_about
;A() != something_A_knows_nothing_about
termina podría , o cualquier otro valor de retorno.True
, perosomething_A_knows_nothing_about != A()
False
El problema con
not self == other
es más sutil. Será correcto para el 99% de las clases, incluidas todas las clases para las que
__ne__
es el inverso lógico de__eq__
. Peronot self == other
rompe las dos reglas mencionadas anteriormente, lo que significa que para las clases donde__ne__
no es el inverso lógico de__eq__
, los resultados son una vez más no simétricos, porque uno de los operandos nunca se pregunta si puede implementar__ne__
en absoluto, incluso si el otro operando no puede. El ejemplo más simple es una clase de bicho raro que devuelveFalse
para todas las comparaciones, por lo queA() == Incomparable()
yA() != Incomparable()
tanto la rentabilidadFalse
. Con una implementación correcta deA.__ne__
(una que regresaNotImplemented
cuando no sabe cómo hacer la comparación), la relación es simétrica;A() != Incomparable()
yIncomparable() != A()
acordar el resultado (porque en el primer caso,A.__ne__
devuelveNotImplemented
, luego , mientras que en el segundo, vuelve directamente). Pero cuando se implementa como , devuelve (porque devuelve, no , luego regresa e invierte eso ), mientras que devuelveIncomparable.__ne__
regresaFalse
Incomparable.__ne__
False
A.__ne__
return not self == other
A() != Incomparable()
True
A.__eq__
NotImplemented
Incomparable.__eq__
False
A.__ne__
True
Incomparable() != A()
False.
Puedes ver un ejemplo de esto en acción aquí .
Evidentemente, una clase que siempre vuelve
False
para ambos__eq__
y__ne__
es un poco extraña. Pero como se mencionó anteriormente,__eq__
y__ne__
ni siquiera es necesario regresarTrue
/False
; el SQLAlchemy ORM tiene clases con comparadores que devuelven un objeto proxy especial para la construcción de consultas, noTrue
/False
en absoluto (son "veraces" si se evalúan en un contexto booleano, pero nunca se supone que deben evaluarse en tal contexto).Si no se sobrecarga
__ne__
correctamente, se romperán clases de ese tipo, ya que el código:funcionará (suponiendo que SQLAlchemy sepa cómo insertar
MyClassWithBadNE
en una cadena SQL; esto se puede hacer con adaptadores de tipo sinMyClassWithBadNE
tener que cooperar en absoluto), pasando el objeto proxy esperado afilter
, mientras:terminará pasando
filter
un simpleFalse
, porqueself == other
devuelve un objeto proxy ynot self == other
simplemente convierte el objeto proxy verdadero enFalse
. Con suerte,filter
arroja una excepción al manejar argumentos inválidos comoFalse
. Si bien estoy seguro de que muchos argumentarán queMyTable.fieldname
debería estar consistentemente en el lado izquierdo de la comparación, el hecho es que no hay una razón programática para hacer cumplir esto en el caso general, y un genérico correcto__ne__
funcionará de cualquier manera, mientras quereturn not self == other
solo funciona en un arreglo.fuente
Respuesta corta: sí (pero lea la documentación para hacerlo bien)
La implementación del
__ne__
método de ShadowRanger es la correcta (y resulta ser la implementación predeterminada del__ne__
método desde Python 3.4):¿Por qué? Porque mantiene una propiedad matemática importante, la simetría del
!=
operador. Este operador es binario, por lo que su resultado debería depender del tipo dinámico de ambos operandos, no solo de uno. Esto se implementa mediante el envío doble para lenguajes de programación que permiten el envío múltiple (como Julia ). En Python, que solo permite el envío único, se simula el envío doble para métodos numéricos y métodos de comparación enriquecidos al devolver el valorNotImplemented
en los métodos de implementación que no admiten el tipo del otro operando; el intérprete intentará entonces el método reflejado del otro operando.La implementación
not self == other
del__ne__
método de Aaron Hall es incorrecta ya que elimina la simetría del!=
operador. De hecho, nunca puede regresarNotImplemented
(not NotImplemented
esFalse
) y, por lo tanto, el__ne__
método con mayor prioridad nunca puede recurrir al__ne__
método con menor prioridad.not self == other
solía ser la implementación predeterminada de Python 3 del__ne__
método, pero era un error que se corrigió en Python 3.4 en enero de 2015, como lo notó ShadowRanger (consulte el número 21408 ).Implementación de los operadores de comparación
La Referencia del lenguaje Python para Python 3 establece en su capítulo III Modelo de datos :
Traducir esto al código Python da (usando
operator_eq
for==
,operator_ne
for!=
,operator_lt
for<
,operator_gt
for>
,operator_le
for<=
yoperator_ge
for>=
):Implementación predeterminada de los métodos de comparación
La documentación agrega:
La implementación por defecto de los métodos de comparación (
__eq__
,__ne__
,__lt__
,__gt__
,__le__
y__ge__
) puede pues ser dada por:Entonces esta es la implementación correcta del
__ne__
método. Y no siempre devuelve el inverso del__eq__
método porque cuando el__eq__
método regresaNotImplemented
, su inversonot NotImplemented
esFalse
(comobool(NotImplemented)
estáTrue
) en lugar del deseadoNotImplemented
.Implementaciones incorrectas de
__ne__
Como Aaron Hall demostró anteriormente,
not self.__eq__(other)
no es la implementación predeterminada del__ne__
método. Pero tampoco lo esnot self == other
. Esto último se demuestra a continuación comparando el comportamiento de la implementación predeterminada con el comportamiento de lanot self == other
implementación en dos casos:__eq__
método regresaNotImplemented
;__eq__
método devuelve un valor diferente deNotImplemented
.Implementación predeterminada
Veamos qué sucede cuando el
A.__ne__
método usa la implementación predeterminada y elA.__eq__
método regresaNotImplemented
:!=
llamadasA.__ne__
.A.__ne__
llamadasA.__eq__
.A.__eq__
devuelveNotImplemented
.!=
llamadasB.__ne__
.B.__ne__
devuelve"B.__ne__"
.Esto muestra que cuando el
A.__eq__
método regresaNotImplemented
, elA.__ne__
método recurre alB.__ne__
método.Ahora veamos qué sucede cuando el
A.__ne__
método usa la implementación predeterminada y elA.__eq__
método devuelve un valor diferente deNotImplemented
:!=
llamadasA.__ne__
.A.__ne__
llamadasA.__eq__
.A.__eq__
devuelveTrue
.!=
devuelvenot True
, eso esFalse
.Esto muestra que en este caso, el
A.__ne__
método devuelve el inverso delA.__eq__
método. Por lo tanto, el__ne__
método se comporta como se anuncia en la documentación.Anular la implementación predeterminada del
A.__ne__
método con la implementación correcta dada anteriormente produce los mismos resultados.not self == other
implementaciónVeamos qué sucede al anular la implementación predeterminada del
A.__ne__
método con lanot self == other
implementación y elA.__eq__
método devuelveNotImplemented
:!=
llamadasA.__ne__
.A.__ne__
llamadas==
.==
llamadasA.__eq__
.A.__eq__
devuelveNotImplemented
.==
llamadasB.__eq__
.B.__eq__
devuelveNotImplemented
.==
devuelveA() is B()
, eso esFalse
.A.__ne__
devuelvenot False
, eso esTrue
.Se
__ne__
devolvió la implementación predeterminada del método"B.__ne__"
, noTrue
.Ahora veamos qué sucede cuando se reemplaza la implementación predeterminada del
A.__ne__
método con lanot self == other
implementación y elA.__eq__
método devuelve un valor diferente deNotImplemented
:!=
llamadasA.__ne__
.A.__ne__
llamadas==
.==
llamadasA.__eq__
.A.__eq__
devuelveTrue
.A.__ne__
devuelvenot True
, eso esFalse
.La implementación predeterminada del
__ne__
método también se devolvióFalse
en este caso.Dado que esta implementación no replica el comportamiento de la implementación predeterminada del
__ne__
método cuando el__eq__
método regresaNotImplemented
, es incorrecta.fuente
__ne__
método cuando el__eq__
método devuelve NotImplemented, es incorrecta". -A
define la igualdad incondicional. Por lo tanto,A() == B()
. AsíA() != B()
debería ser Falso , y lo es . Los ejemplos dados son patológicos (es decir__ne__
, no deben devolver una cadena, y__eq__
no deben depender de__ne__
, sino que__ne__
deben depender de__eq__
, que es la expectativa predeterminada en Python 3). Todavía estoy -1 en esta respuesta hasta que puedas cambiar de opinión.NotImplemented
si no implementa la operación para un par de argumentos dado. Por convención,False
yTrue
se devuelven para una comparación exitosa. Sin embargo, estos métodos pueden devolver cualquier valor , por lo que si el operador de comparación se usa en un contexto booleano (por ejemplo, en la condición de una declaración if), Python llamarábool()
al valor para determinar si el resultado es verdadero o falso ".__ne__
mata una propiedad matemática importante, la simetría del!=
operador. Este operador es binario, por lo que su resultado debería depender del tipo dinámico de ambos operandos, no solo de uno. Esto se implementa correctamente en los lenguajes de programación a través de un envío doble para el lenguaje que permite el envío múltiple . En Python, que solo permite el envío único, el envío doble se simula devolviendo elNotImplemented
valor.B
que devuelve una cadena veraz en todas las comprobaciones de__ne__
yA
que devuelveTrue
todas las comprobaciones de__eq__
. Esta es una contradicción patológica. En tal contradicción, sería mejor plantear una excepción. Sin conocimiento deB
,A
no tiene la obligación de respetarB
la implementación de__ne__
a los efectos de la simetría. En ese punto del ejemplo, cómo seA
implementa__ne__
es irrelevante para mí. Encuentre un caso práctico, no patológico para exponer su punto. He actualizado mi respuesta para dirigirme a ti.__ne__
trabajos en casos de uso típicos no lo hace correcto. Los aviones Boeing 737 MAX volaron 500.000 vuelos antes de los accidentes…Si todos
__eq__
,__ne__
,__lt__
,__ge__
,__le__
, y__gt__
tienen sentido para la clase, a continuación, sólo aplicar__cmp__
en su lugar. De lo contrario, haz lo que estás haciendo, por lo que dijo Daniel DiPaolo (mientras lo probaba en lugar de buscarlo;))fuente
__cmp__()
método especial ya no es compatible con Python 3.x, por lo que debería acostumbrarse a usar los operadores de comparación enriquecidos.