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.NotImplementedretorno 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 devuelveNotImplementedel 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
otheres un subtipo, y si tiene que definido por el operador, se utiliza elothermétodo 's primero (inversa para<,<=,>=y>). SiNotImplementedse 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.cenobject_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 frenteNotImplementeden__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_pythonestá 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 nuncaNotImplementedno lo hace incorrecto. En cambio, manejamos la priorización con aNotImplementedtravé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 verificarseisy 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
NotImplementedde__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 != c2regresóFalse--- no se ejecutóa1.__ne__, peroc2.__ne__, lo que negó el método de Mixin__eq__. Ya queNotImplementedes verdad,not NotImplementedesFalse.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 niTrueoFalse, 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 denoton__eq__(por ejemplo, el ORM de SQLAlchemy, donde ambos__eq__y__ne__devuelven objetos proxy especiales, noTrueoFalse, y tratandonotde__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, porqueNotImplementedes 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 usaNotImplementeddevoluciones, esto funciona (con una sobrecarga sin sentido), si se usa aNotImplementedveces, 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: siRHSes una subclase deLHSla 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 siRHSes una subclase deLHSla clase de)LHS.__eq__(RHS)devolverTrueno 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_aboutregresaFalse, cuando debería haber verificado sisomething_A_knows_nothing_aboutsabí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 enFalselugar deNotImplementedcuando no reconoce al otro lado), entonces esto es "correcto" desdeAla perspectiva de, regresandoTrue(desdeAque no cree que sea igual, entonces no es igual), pero podría ser mal desomething_A_knows_nothing_aboutla perspectiva, ya que ni siquiera preguntósomething_A_knows_nothing_about;A() != something_A_knows_nothing_abouttermina podría , o cualquier otro valor de retorno.True, perosomething_A_knows_nothing_about != A()FalseEl problema con
not self == otheres 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 == otherrompe 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 devuelveFalsepara todas las comparaciones, por lo queA() == Incomparable()yA() != Incomparable()tanto la rentabilidadFalse. Con una implementación correcta deA.__ne__(una que regresaNotImplementedcuando 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__regresaFalseIncomparable.__ne__FalseA.__ne__return not self == otherA() != Incomparable()TrueA.__eq__NotImplementedIncomparable.__eq__FalseA.__ne__TrueIncomparable() != A()False.Puedes ver un ejemplo de esto en acción aquí .
Evidentemente, una clase que siempre vuelve
Falsepara 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/Falseen 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
MyClassWithBadNEen una cadena SQL; esto se puede hacer con adaptadores de tipo sinMyClassWithBadNEtener que cooperar en absoluto), pasando el objeto proxy esperado afilter, mientras:terminará pasando
filterun simpleFalse, porqueself == otherdevuelve un objeto proxy ynot self == othersimplemente convierte el objeto proxy verdadero enFalse. Con suerte,filterarroja una excepción al manejar argumentos inválidos comoFalse. Si bien estoy seguro de que muchos argumentarán queMyTable.fieldnamedeberí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 == othersolo 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 valorNotImplementeden 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 == otherdel__ne__método de Aaron Hall es incorrecta ya que elimina la simetría del!=operador. De hecho, nunca puede regresarNotImplemented(not NotImplementedesFalse) y, por lo tanto, el__ne__método con mayor prioridad nunca puede recurrir al__ne__método con menor prioridad.not self == othersolí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_eqfor==,operator_nefor!=,operator_ltfor<,operator_gtfor>,operator_lefor<=yoperator_gefor>=):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 NotImplementedesFalse(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 == otherimplementació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 == otherimplementaciónVeamos qué sucede al anular la implementación predeterminada del
A.__ne__método con lanot self == otherimplementació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 == otherimplementació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óFalseen 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". -Adefine 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.NotImplementedsi no implementa la operación para un par de argumentos dado. Por convención,FalseyTruese 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 elNotImplementedvalor.Bque devuelve una cadena veraz en todas las comprobaciones de__ne__yAque devuelveTruetodas 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,Ano tiene la obligación de respetarBla implementación de__ne__a los efectos de la simetría. En ese punto del ejemplo, cómo seAimplementa__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.