__eq__ - equal python
Formas elegantes de apoyar la equivalencia("igualdad") en las clases de Python (8)
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 documentation :
No hay relaciones implícitas entre los operadores de comparación. La verdad de
x==y
no implica quex!=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 documentation :
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<y or x==y)
no implica quex<=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 classic-style (o de estilo antiguo ), que no heredan de un
object
y que se declaran comoclass A:
class A():
oclass A(B):
dondeB
es una clase de estilo clásico;clases de classic-style , que sí heredan de un
object
y que se declaran comoclass A(object)
oclass A(B):
dondeB
es una clase de nuevo estilo. Python 3 solo tiene clases de estilo nuevo que se declaran comoclass A:
class A(object):
oclass 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
llaman1.__eq__
; -
n3 == n1
llaman3.__eq__
; -
n1 != n3
llaman1.__ne__
; -
n3 != n1
llaman3.__ne__
.
Y si Number
es una clase de nuevo estilo:
- tanto
n1 == n3
comon3 == n1
llamann3.__eq__
; - ambos
n1 != n3
yn3 != n1
llamann3.__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 un tipo de operando no es compatible. La documentation 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 documentation 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 conmutatividad 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
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
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
De esta respuesta: https://.com/a/30676267/541136 Lo he demostrado, mientras que 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
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
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).
Necesitas 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.
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 ...
Dada una clase que define uno o más métodos de orden de comparación ricos, este decorador de clase proporciona el resto. Esto simplifica el esfuerzo involucrado en la especificación de todas las posibles operaciones de comparación enriquecidas:
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__
.