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==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.
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 deobject
y que se declaran comoclass A:
,class A():
oclass A(B):
dondeB
es una clase de estilo clásico;
clases de estilo nuevo , que heredan deobject
y que se declaran comoclass A(object)
oclass A(B):
dondeB
es 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 Number
es una clase de estilo clásico:
n1 == n3
llamadas n1.__eq__
;
n3 == n1
llamadas n3.__eq__
;
n1 != n3
llamadas n1.__ne__
;
n3 != n1
llamadas n3.__ne__
.
Y si Number
es una clase de nuevo estilo:
- ambos
n1 == n3
y n3 == n1
llamar n3.__eq__
;
- ambos
n1 != n3
y n3 != n1
llama 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 NotImplemented
valor cuando no se admite un tipo de operando. La documentación define el NotImplemented
valor 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 NotImplemented
valor en 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 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
is
operador para distinguir la identidad del objeto de la comparación de valores.