Formas elegantes de apoyar la equivalencia (“igualdad”) en las clases de Python

Cuando se escriben clases personalizadas, a menudo es importante permitir la equivalencia por medio de los operadores == y != . En Python, esto es posible implementando los métodos especiales __eq__ y __ne__ , respectivamente. La forma más fácil que he encontrado para hacer esto es el siguiente método:

 class Foo: def __init__(self, item): self.item = item def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ else: return False def __ne__(self, other): return not self.__eq__(other) 

¿Conoces medios más elegantes para hacer esto? ¿Conoce alguna desventaja particular al usar el método anterior para comparar __dict__ s?

Nota : un poco de aclaración: cuando __eq__ y __ne__ no están definidos, encontrará este comportamiento:

 >>> a = Foo(1) >>> b = Foo(1) >>> a is b False >>> a == b False 

Es decir, a == b evalúa como False porque realmente ejecuta a is b , una prueba de identidad (es decir, “¿Es el mismo objeto que b ?”).

Cuando se definen __eq__ y __ne__ , encontrará este comportamiento (que es el que buscamos):

 >>> a = Foo(1) >>> b = Foo(1) >>> a is b False >>> a == b True 

Considera este simple problema:

 class Number: def __init__(self, number): self.number = number n1 = Number(1) n2 = Number(1) n1 == n2 # False -- oops 

Por lo tanto, Python utiliza de forma predeterminada los identificadores de objetos para las operaciones de comparación:

 id(n1) # 140400634555856 id(n2) # 140400634555920 

La __eq__ función __eq__ parece resolver el problema:

 def __eq__(self, other): """Overrides the default implementation""" if isinstance(other, Number): return self.number == other.number return False n1 == n2 # True n1 != n2 # True in Python 2 -- oops, False in Python 3 

En Python 2 , recuerde siempre anular la función __ne__ también, como indica la documentación :

No hay relaciones implícitas entre los operadores de comparación. La verdad de x==y no implica que x!=y sea ​​falsa. En consecuencia, al definir __eq__() , también se debe definir __ne__() para que los operadores se comporten como se espera.

 def __ne__(self, other): """Overrides the default implementation (unnecessary in Python 3)""" return not self.__eq__(other) n1 == n2 # True n1 != n2 # False 

En Python 3 , esto ya no es necesario, como indica la documentación :

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

Pero eso no resuelve todos nuestros problemas. Vamos a añadir una subclase:

 class SubNumber(Number): pass n3 = SubNumber(1) n1 == n3 # False for classic-style classes -- oops, True for new-style classes n3 == n1 # True n1 != n3 # True for classic-style classes -- oops, False for new-style classes n3 != n1 # False 

Nota: Python 2 tiene dos tipos de clases:

  • clases de estilo clásico (o de estilo antiguo ), que no heredan de un object y que se declaran como class A: class A(): o class A(B): donde B es una clase de estilo clásico;

  • clases de nuevo estilo , que sí heredan de un object y que se declaran como class A(object) o class A(B): donde B es una clase de nuevo estilo. Python 3 solo tiene clases de estilo nuevo que se declaran como class A: class A(object): o class A(B):

Para las clases de estilo clásico, una operación de comparación siempre llama al método del primer operando, mientras que para las clases de estilo nuevo, siempre llama al método del operando de subclase, independientemente del orden de los operandos .

Así que aquí, si Number es una clase de estilo clásico:

  • n1 == n3 llama n1.__eq__ ;
  • n3 == n1 llama n3.__eq__ ;
  • n1 != n3 llama n1.__ne__ ;
  • n3 != n1 llama n3.__ne__ .

Y si Number es una clase de nuevo estilo:

  • tanto n1 == n3 como n3 == n1 llaman n3.__eq__ ;
  • ambos n1 != n3 y n3 != n1 llaman n3.__ne__ .

Para solucionar el problema de no conmutatividad de los operadores == y != Para las clases de estilo clásico de Python 2, los métodos __eq__ y __ne__ deben devolver el valor NotImplemented cuando no se admita un tipo de operando. La documentación define el valor NotImplemented como:

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 luego intentará la operación reflejada, o algún otro respaldo, dependiendo del operador). Su valor de verdad es verdadero.

En este caso, el operador delega la operación de comparación al método reflejado del otro operando. La documentación define los métodos reflejados como:

No hay versiones con argumento de intercambio de estos métodos (que se utilizarán cuando el argumento de la izquierda no sea compatible con la operación, pero sí el argumento de la derecha); más bien, __lt__() y __gt__() son la reflexión de cada uno, __le__() y __ge__() son la reflexión de cada uno, y __eq__() y __ne__() son su propia reflexión.

El resultado se ve así:

 def __eq__(self, other): """Overrides the default implementation""" if isinstance(other, Number): return self.number == other.number return NotImplemented def __ne__(self, other): """Overrides the default implementation (unnecessary in Python 3)""" x = self.__eq__(other) if x is not NotImplemented: return not x return NotImplemented 

Devolver el valor No NotImplemented lugar de False es lo correcto, incluso para las clases de estilo nuevo si se desea la conmutabilidad de los operadores == y != Cuando los operandos son de tipos no relacionados (sin herencia).

¿Ya llegamos? No exactamente. ¿Cuántos números únicos tenemos?

 len(set([n1, n2, n3])) # 3 -- oops 

Los conjuntos utilizan los hashes de los objetos y, de forma predeterminada, Python devuelve el hash del identificador del objeto. Vamos a tratar de anularlo:

 def __hash__(self): """Overrides the default implementation""" return hash(tuple(sorted(self.__dict__.items()))) len(set([n1, n2, n3])) # 1 

El resultado final se ve así (agregué algunas aserciones al final para la validación):

 class Number: def __init__(self, number): self.number = number def __eq__(self, other): """Overrides the default implementation""" if isinstance(other, Number): return self.number == other.number return NotImplemented def __ne__(self, other): """Overrides the default implementation (unnecessary in Python 3)""" x = self.__eq__(other) if x is not NotImplemented: return not x return NotImplemented def __hash__(self): """Overrides the default implementation""" return hash(tuple(sorted(self.__dict__.items()))) class SubNumber(Number): pass n1 = Number(1) n2 = Number(1) n3 = SubNumber(1) n4 = SubNumber(4) assert n1 == n2 assert n2 == n1 assert not n1 != n2 assert not n2 != n1 assert n1 == n3 assert n3 == n1 assert not n1 != n3 assert not n3 != n1 assert not n1 == n4 assert not n4 == n1 assert n1 != n4 assert n4 != n1 assert len(set([n1, n2, n3, ])) == 1 assert len(set([n1, n2, n3, n4])) == 2 

Tienes que tener cuidado con la herencia:

 >>> class Foo: def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ else: return False >>> class Bar(Foo):pass >>> b = Bar() >>> f = Foo() >>> f == b True >>> b == f False 

Compruebe los tipos más estrictamente, como este:

 def __eq__(self, other): if type(other) is type(self): return self.__dict__ == other.__dict__ return False 

Además de eso, su enfoque funcionará bien, para eso están los métodos especiales.

La forma en que lo describe es la forma en que siempre lo he hecho. Ya que es totalmente genérico, siempre puede dividir esa funcionalidad en una clase mixta y heredarla en las clases en las que desea esa funcionalidad.

 class CommonEqualityMixin(object): def __eq__(self, other): return (isinstance(other, self.__class__) and self.__dict__ == other.__dict__) def __ne__(self, other): return not self.__eq__(other) class Foo(CommonEqualityMixin): def __init__(self, item): self.item = item 

No era una respuesta directa, pero parecía lo suficientemente relevante como para ser abordado, ya que en ocasiones ahorra un poco de tedio detallado. Cortar directamente de los documentos …


functools.total_ordering (cls)

Dada una clase que define uno o más métodos de orden de comparación ricos, este decorador de clase proporciona el rest. Esto simplifica el esfuerzo involucrado en la especificación de todas las operaciones de comparación ricas posibles:

La clase debe definir uno de lt (), le (), gt () o ge (). Además, la clase debe proporcionar un método eq ().

Nuevo en la versión 2.7

 @total_ordering class Student: def __eq__(self, other): return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) def __lt__(self, other): return ((self.lastname.lower(), self.firstname.lower()) < (other.lastname.lower(), other.firstname.lower())) 

No tiene que anular tanto __eq__ como __ne__ , solo puede anular __cmp__ pero esto implicará el resultado de == __cmp__ ==, <,> y así sucesivamente.

Pruebas de identidad de objeto. Esto significa que a is b será True en el caso de que ayb mantengan la referencia al mismo objeto. En Python, siempre tiene una referencia a un objeto en una variable que no es el objeto real, por lo que esencialmente para que a es b sea verdadero, los objetos que se encuentran en ellos deben ubicarse en la misma ubicación de memoria. ¿Cómo y, lo más importante, por qué diría usted sobre este comportamiento?

Edición: no sabía que __cmp__ se eliminó de Python 3, así que __cmp__ .

De esta respuesta: https://stackoverflow.com/a/30676267/541136 He demostrado que, si bien es correcto definir __ne__ en términos __eq__ – en lugar de

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

Deberías usar:

 def __ne__(self, other): return not self == other 

Creo que los dos términos que estás buscando son igualdad (==) e identidad (es). Por ejemplo:

 >>> a = [1,2,3] >>> b = [1,2,3] >>> a == b True <-- a and b have values which are equal >>> a is b False <-- a and b are not the same list object 

La prueba ‘is’ probará la identidad usando la función incorporada ‘id ()’ que esencialmente devuelve la dirección de memoria del objeto y, por lo tanto, no es recargable.

Sin embargo, en el caso de probar la igualdad de una clase, es probable que desee ser un poco más estricto con sus pruebas y solo comparar los atributos de datos en su clase:

 import types class ComparesNicely(object): def __eq__(self, other): for key, value in self.__dict__.iteritems(): if (isinstance(value, types.FunctionType) or key.startswith("__")): continue if key not in other.__dict__: return False if other.__dict__[key] != value: return False return True 

Este código solo comparará miembros de datos no funcionales de su clase, así como omitir cualquier cosa privada que generalmente es lo que usted desea. En el caso de los objetos de Python viejos y llanos, tengo una clase base que implementa __init__, __str__, __repr__ y __eq__, por lo que mis objetos POPO no soportan la carga de toda esa lógica adicional (y en la mayoría de los casos, idéntica).

En lugar de usar subclases / mixins, me gusta usar un decorador de clase genérico

 def comparable(cls): """ Class decorator providing generic comparison functionality """ def __eq__(self, other): return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ def __ne__(self, other): return not self.__eq__(other) cls.__eq__ = __eq__ cls.__ne__ = __ne__ return cls 

Uso:

 @comparable class Number(object): def __init__(self, x): self.x = x a = Number(1) b = Number(1) assert a == b