Python, ¿debería implementar el operador __ne __ () basado en __eq__?


98

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?

Respuestas:


57

Sí, está perfectamente bien. De hecho, la documentación le insta a definir __ne__cuándo define __eq__:

No hay relaciones implícitas entre los operadores de comparación. La verdad de x==yno 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.

En muchos casos (como este), será tan simple como negar el resultado de __eq__, pero no siempre.


12
esta es la respuesta correcta (aquí abajo, por @ aaron-hall). La documentación que citó no lo alienta a implementar el __ne__uso __eq__, solo que lo implemente.
guyarad

2
@guyarad: En realidad, la respuesta de Aaron sigue siendo un poco incorrecta gracias a que no delegó adecuadamente; en lugar de tratar un 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 .
ShadowRanger

1
La crítica de ShadowRanger solo se aplicaría a casos muy patológicos (en mi humilde opinión) y se aborda completamente en mi respuesta a continuación.
Aaron Hall

1
La documentación más reciente (para 3.7 al menos, podría ser incluso anterior) __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.
bluesummers

132

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==yno implica que x!=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 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 NotImplementedel 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 otheres un subtipo, y si tiene que definido por el operador, se utiliza el othermétodo 's primero (inversa para <, <=, >=y >). Si NotImplementedse 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.cenobject_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 NotImplementeden __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_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:

La implementación not self == otherde Aaron Hall del __ne__método es incorrecta, ya que nunca puede regresar NotImplemented( not NotImplementedes 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 NotImplementedno lo hace incorrecto. En cambio, manejamos la priorización con a NotImplementedtravés de la verificación de igualdad con ==. Suponiendo que ==se implemente correctamente, hemos terminado.

not self == othersolí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 isy 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 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:

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.


3
¡Excelentes ejemplos! Parte de la sorpresa es que el orden de los operandos no importa en absoluto , a diferencia de algunos métodos mágicos con sus reflejos del "lado derecho". Para repetir la parte que me perdí (y que me costó mucho tiempo): el método de comparación enriquecido de la subclase se prueba primero, independientemente de si el código tiene la superclase o la subclase a la izquierda del operador. Es por eso que a1 != c2regresó False--- no se ejecutó a1.__ne__, pero c2.__ne__, lo que negó el método de Mixin __eq__ . Ya que NotImplementedes verdad, not NotImplementedes False.
Kevin J. Chase

2
Sus actualizaciones recientes demuestran con éxito la ventaja de rendimiento de 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 hacerlo not (self == other), pero cambió porque era incorrecto en presencia de subclases arbitrarias . Lo más rápido para la respuesta incorrecta sigue siendo incorrecta .
ShadowRanger

1
El ejemplo específico realmente no es importante. El problema es que, en su implementación, el comportamiento de sus __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 regresa NotImplemented, 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).
ShadowRanger

1
@AaronHall: Al reexaminar esto hoy, no creo que su implementación sea problemática para las subclases normalmente (sería extremadamente complicado hacer que se rompa, y la subclase, que se supone que tiene pleno conocimiento del padre, debería poder evitarlo ). Pero solo di un ejemplo sencillo en mi respuesta. El caso no patológico es el ORM de SQLAlchemy, donde ni __eq__ni __ne__devuelve ni Trueo False, 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).
ShadowRanger

1
Para ser claros, en el 99% (o tal vez el 99,999%) de los casos, su solución está bien y (obviamente) más rápida. Pero dado que no tiene control sobre los casos en los que no está bien, como escritor de bibliotecas cuyo código puede ser utilizado por otros (léase: cualquier cosa menos scripts y módulos simples para uso personal), debe utilice la implementación correcta para adherirse al contrato general para la sobrecarga del operador y trabaje con cualquier otro código que pueda encontrar. Afortunadamente, en Py3, nada de esto importa, ya que puede omitirlo por __ne__completo. Dentro de un año, Py2 estará muerto y lo ignoramos. :-)
ShadowRanger

10

Solo para el registro, un portátil Py2 / Py3 canónicamente correcto y cruzado __ne__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 cualquiera __eq__que pueda definir:

  • A diferencia de 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 de noton __eq__(por ejemplo, el ORM de SQLAlchemy, donde ambos __eq__y __ne__devuelven objetos proxy especiales, no Trueo False, y tratando notde __eq__devolver el resultado de False, en lugar del objeto proxy correcto).
  • A diferencia not self.__eq__(other), esto delega correctamente a la __ne__de la otra instancia cuando self.__eq__retorna NotImplemented( not self.__eq__(other)sería extra incorrecto, porque NotImplementedes veraz, entonces cuando __eq__no supiera cómo realizar la comparación, __ne__regresaría False, 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 usa NotImplementeddevoluciones, esto funciona (con una sobrecarga sin sentido), si se usa a NotImplementedveces, 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:

  1. (Se aplica a todos los operadores) Cuando esté en ejecución LHS OP RHS, intente LHS.__op__(RHS), y si vuelve NotImplemented, intente RHS.__rop__(LHS). Excepción: si RHSes una subclase de LHSla clase de, pruebe RHS.__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__es LHS.__ne__(RHS), entonces RHS.__ne__(LHS), invertido si RHSes una subclase de LHSla clase de)
  2. Aparte de la idea del operador "intercambiado", no existe una relación implícita entre los operadores. Incluso, por ejemplo, de la misma clase, LHS.__eq__(RHS)devolver Trueno implica LHS.__ne__(RHS)retornos False(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:

  1. Si sabe cómo implementar la operación usted mismo, hágalo utilizando solo su propio conocimiento de cómo hacer la comparación (nunca delegue, implícita o explícitamente, al otro lado de la operación; hacerlo corre el riesgo de incorrección y / o recursividad infinita, dependiendo de como lo hagas)
  2. Si no sabe cómo implementar la operación usted mismo, siempre regrese NotImplemented, para que Python pueda delegar en la implementación del otro operando

El problema con not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

nunca delega al otro lado (y es incorrecto si __eq__regresa correctamente NotImplemented). Cuando self.__eq__(other)regresa NotImplemented(que es "veraz"), regresa silenciosamente False, entonces A() != something_A_knows_nothing_aboutregresa False, cuando debería haber verificado si something_A_knows_nothing_aboutsabía cómo comparar con instancias de A, y si no lo hace, debería haber regresado True(ya que si ninguna de las partes sabe cómo en comparación con el otro, se consideran no iguales entre sí). Si A.__eq__está implementado incorrectamente (regresando en Falselugar de NotImplementedcuando no reconoce al otro lado), entonces esto es "correcto" desde Ala perspectiva de, regresando True(desdeA que 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()False

El problema con not self == other

def __ne__(self, other):
    return 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__. Pero not 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 devuelve Falsepara todas las comparaciones, por lo que A() == Incomparable()y A() != Incomparable()tanto la rentabilidad False. Con una implementación correcta de A.__ne__(una que regresa NotImplementedcuando 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__devuelve NotImplemented, 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 regresar True/ False; el SQLAlchemy ORM tiene clases con comparadores que devuelven un objeto proxy especial para la construcción de consultas, no True/ 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:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

funcionará (suponiendo que SQLAlchemy sepa cómo insertar MyClassWithBadNEen una cadena SQL; esto se puede hacer con adaptadores de tipo sin MyClassWithBadNEtener que cooperar en absoluto), pasando el objeto proxy esperado a filter, mientras:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

terminará pasando filterun simple False, porque self == otherdevuelve un objeto proxy y not self == othersimplemente convierte el objeto proxy verdadero en False. Con suerte, filterarroja una excepción al manejar argumentos inválidos como False. Si bien estoy seguro de que muchos argumentarán que MyTable.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 que return not self == othersolo funciona en un arreglo.


1
La única respuesta correcta, completa y honesta (lo siento @AaronHall). Esta debería ser la respuesta aceptada.
Maggyero

4

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):

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

¿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 valor NotImplementeden 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 regresar NotImplemented( not NotImplementedes False) 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 :

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<yllamadas x.__lt__(y), x<=yllamadas x.__le__(y), x==yllamadas x.__eq__(y), x!=yllamadas x.__ne__(y), x>yllamadas x.__gt__(y)y x>=y llamadasx.__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 el de la derecha sí); más bien, __lt__()y __gt__()son el reflejo del otro, __le__()y __ge__()son el reflejo del otro, __eq__()y __ne__()son su propio reflejo. Si los operandos son de diferentes tipos, y el tipo del 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. No se considera la subclasificación virtual.

Traducir esto al código Python da (usando operator_eqfor ==, operator_nefor !=, operator_ltfor <, operator_gtfor >, operator_lefor <=y operator_gefor >=):

def operator_eq(left, right):
    if type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 predeterminada de los métodos de comparación

La documentación agrega:

De forma predeterminada, __ne__()delega __eq__()e invierte el resultado a menos que lo sea NotImplemented. No hay otras relaciones implícitas entre los operadores de comparación, por ejemplo, la verdad de (x<y or x==y)no implica x<=y.

La implementación por defecto de los métodos de comparación ( __eq__, __ne__, __lt__, __gt__, __le__y __ge__) puede pues 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 __ne__método. Y no siempre devuelve el inverso del __eq__método porque cuando el __eq__método regresa NotImplemented, su inverso not NotImplementedes False(como bool(NotImplemented)está True) en lugar del deseado NotImplemented.

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 es not self == other. Esto último se demuestra a continuación comparando el comportamiento de la implementación predeterminada con el comportamiento de la not self == otherimplementación en dos casos:

  • el __eq__método regresa NotImplemented;
  • el __eq__método devuelve un valor diferente de NotImplemented.

Implementación predeterminada

Veamos qué sucede cuando el A.__ne__método usa la implementación predeterminada y el A.__eq__método regresa NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. !=llamadas A.__ne__.
  2. A.__ne__llamadas A.__eq__.
  3. A.__eq__devuelve NotImplemented.
  4. !=llamadas B.__ne__.
  5. B.__ne__devuelve "B.__ne__".

Esto muestra que cuando el A.__eq__método regresa NotImplemented, el A.__ne__método recurre al B.__ne__método.

Ahora veamos qué sucede cuando el A.__ne__método usa la implementación predeterminada y el A.__eq__método 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
  1. !=llamadas A.__ne__.
  2. A.__ne__llamadas A.__eq__.
  3. A.__eq__devuelve True.
  4. !=devuelve not True, eso es False.

Esto muestra que en este caso, el A.__ne__método devuelve el inverso del A.__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ón

Veamos qué sucede al anular la implementación predeterminada del A.__ne__método con la not self == otherimplementación y el A.__eq__método 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
  1. !=llamadas A.__ne__.
  2. A.__ne__llamadas ==.
  3. ==llamadas A.__eq__.
  4. A.__eq__devuelve NotImplemented.
  5. ==llamadas B.__eq__.
  6. B.__eq__devuelve NotImplemented.
  7. ==devuelve A() is B(), eso es False.
  8. A.__ne__devuelve not False, eso es True.

Se __ne__devolvió la implementación predeterminada del método "B.__ne__", no True.

Ahora veamos qué sucede cuando se reemplaza la implementación predeterminada del A.__ne__método con la not self == otherimplementación y el A.__eq__método 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
  1. !=llamadas A.__ne__.
  2. A.__ne__llamadas ==.
  3. ==llamadas A.__eq__.
  4. A.__eq__devuelve True.
  5. A.__ne__devuelve not True, eso es False.

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 regresa NotImplemented, es incorrecta.


Para su último ejemplo: "Dado que esta implementación no replica el comportamiento de la implementación predeterminada del __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.
Aaron Hall

@AaronHall De la referencia del lenguaje Python : "Un método de comparación enriquecido puede devolver el singleton NotImplementedsi no implementa la operación para un par de argumentos dado. Por convención, Falsey Truese 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 ".
Maggyero

@AaronHall Su implementación de __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 el NotImplementedvalor.
Maggyero

El ejemplo final tiene dos clases, Bque devuelve una cadena veraz en todas las comprobaciones de __ne__y Aque devuelve Truetodas las comprobaciones de __eq__. Esta es una contradicción patológica. En tal contradicción, sería mejor plantear una excepción. Sin conocimiento de B, Ano tiene la obligación de respetar Bla implementación de __ne__a los efectos de la simetría. En ese punto del ejemplo, cómo se Aimplementa __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.
Aaron Hall

@AaronHall Para obtener un ejemplo más realista, consulte el ejemplo de SQLAlchemy proporcionado por @ShadowRanger. También tenga en cuenta que el hecho de que su implementación de __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…
Maggyero

-1

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;))


12
El __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.
Don O'Donnell

8
O alternativamente, si está en Python 2.7 o 3.x, el decorador functools.total_ordering también es bastante útil.
Adam Parkin

Gracias por el aviso. Sin embargo, me he dado cuenta de muchas cosas en ese sentido en el último año y medio. ;)
Karl Knechtel
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.