Considere este simple problema:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Entonces, Python por defecto usa los identificadores de objeto para las operaciones de comparación:
id(n1) # 140400634555856
id(n2) # 140400634555920
Anular la __eq__función 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 __ne__función también, como dice la documentación :
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.
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 dice la documentación :
Por defecto, __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 (x<y or x==y)no implica x<=y.
Pero eso no resuelve todos nuestros problemas. Agreguemos 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 estilo antiguo ), que no heredan deobjecty que se declaran comoclass A:,class A():oclass A(B):dondeBes una clase de estilo clásico;
clases de estilo nuevo , que heredan deobjecty que se declaran comoclass A(object)oclass A(B):dondeBes una clase de estilo nuevo. 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 la subclase, independientemente del orden de los operandos .
Entonces aquí, si Numberes una clase de estilo clásico:
n1 == n3llamadas n1.__eq__;
n3 == n1llamadas n3.__eq__;
n1 != n3llamadas n1.__ne__;
n3 != n1llamadas n3.__ne__.
Y si Numberes una clase de nuevo estilo:
- ambos
n1 == n3y n3 == n1llamar n3.__eq__;
- ambos
n1 != n3y n3 != n1llama 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__deberían devolver el NotImplementedvalor cuando no se admite un tipo de operando. La documentación define el NotImplementedvalor 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 intentará la operación reflejada, o alguna otra alternativa, 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 de argumentos intercambiados de estos métodos (para usarse cuando el argumento izquierdo no admite la operación pero el argumento derecho 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.
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 NotImplemented:
return NotImplemented
return not x
Devolver el NotImplementedvalor en lugar de Falsees 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 usan los hash de los objetos y, de forma predeterminada, Python devuelve el hash del identificador del objeto. Intentemos 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 afirmaciones 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
isoperador para distinguir la identidad del objeto de la comparación de valores.