magic example data __str__ __getitem__ __eq__ python comparison operators python-datamodel

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 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.

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 haya NotImplemented .

y en la sección "qué hay de nuevo" , vemos que este comportamiento ha cambiado:

  • != ahora devuelve el opuesto de == , a menos que == regrese NotImplemented .

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 llamadas x.__lt__(y) , x<=y llamadas x.__le__(y) , x==y llamadas x.__eq__(y) , x!=y llama a x.__ne__(y) , x>y llama x.__gt__(y) , y x>=y llama x.__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 haya 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 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 devuelve NotImplemented ;
  • el método __eq__ devuelve un valor diferente de NotImplemented .

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__"

  1. != llama A.__ne__ .
  2. A.__ne__ llama A.__eq__ .
  3. A.__eq__ devuelve NotImplemented .
  4. != llama B.__ne__ .
  5. 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

  1. != llama A.__ne__ .
  2. A.__ne__ llama A.__eq__ .
  3. A.__eq__ devuelve True .
  4. != devuelve not True , eso es False .

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

  1. != llama A.__ne__ .
  2. A.__ne__ calls == .
  3. == llama A.__eq__ .
  4. A.__eq__ devuelve NotImplemented .
  5. == llama a B.__eq__ .
  6. B.__eq__ devuelve NotImplemented .
  7. == devuelve A() is B() , eso es False .
  8. A.__ne__ not False , eso es True .

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

  1. != llama A.__ne__ .
  2. A.__ne__ calls == .
  3. == llama A.__eq__ .
  4. A.__eq__ devuelve True .
  5. A.__ne__ not True , eso es False .

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 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.


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.