Considere este problema simples:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Portanto, o Python, por padrão, usa os identificadores de objeto para operações de comparação:
id(n1) # 140400634555856
id(n2) # 140400634555920
A substituição da __eq__
função parece resolver o 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
No Python 2 , lembre-se sempre de substituir a __ne__
função também, conforme a documentação declara:
Não há relacionamentos implícitos entre os operadores de comparação. A verdade de x==y
não implica que isso x!=y
seja falso. Assim, ao definir __eq__()
, deve-se definir também __ne__()
para que os operadores se comportem conforme o esperado.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
No Python 3 , isso não é mais necessário, pois a documentação declara:
Por padrão, __ne__()
delega __eq__()
e inverte o resultado, a menos que seja NotImplemented
. Não há outras relações implícitas entre os operadores de comparação, por exemplo, a verdade de (x<y or x==y)
não implica x<=y
.
Mas isso não resolve todos os nossos problemas. Vamos adicionar uma subclasse:
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 possui dois tipos de classes:
de estilo clássico (ou de estilo antigo ) classes, que não herdamobject
e que são declarados comoclass A:
,class A():
ouclass A(B):
ondeB
é uma classe de estilo clássico;
classes de novo estilo , que herdamobject
e são declaradas comoclass A(object)
ouclass A(B):
ondeB
é uma classe de novo estilo. O Python 3 possui apenas classes de novo estilo que são declaradas comoclass A:
,class A(object):
ouclass A(B):
.
Para classes de estilo clássico, uma operação de comparação sempre chama o método do primeiro operando, enquanto para classes de novo estilo, sempre chama o método do operando da subclasse, independentemente da ordem dos operandos .
Então, aqui, se Number
é uma classe de estilo clássico:
n1 == n3
chamadas n1.__eq__
;
n3 == n1
chamadas n3.__eq__
;
n1 != n3
chamadas n1.__ne__
;
n3 != n1
chamadas n3.__ne__
.
E se Number
é uma classe de novo estilo:
- ambos
n1 == n3
e n3 == n1
ligar n3.__eq__
;
- ambos
n1 != n3
e n3 != n1
ligue n3.__ne__
.
Para corrigir o problema de não comutatividade dos operadores ==
e !=
para as classes de estilo clássico do Python 2, os métodos __eq__
e __ne__
devem retornar o NotImplemented
valor quando um tipo de operando não for suportado. A documentação define o NotImplemented
valor como:
Métodos numéricos e métodos de comparação avançados podem retornar esse valor se eles não implementarem a operação para os operandos fornecidos. (O intérprete tentará a operação refletida ou algum outro fallback, dependendo do operador.) Seu valor verdadeiro é verdadeiro.
Nesse caso, o operador delega a operação de comparação para o método refletido do outro operando. A documentação define métodos refletidos como:
Não há versões de argumento trocado desses métodos (a serem usadas quando o argumento esquerdo não suporta a operação, mas o argumento correto); sim, __lt__()
e __gt__()
são reflexo um do outro, __le__()
e __ge__()
são reflexo um do outro, e
__eq__()
e __ne__()
são a sua própria reflexão.
O resultado fica assim:
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
Retornar o NotImplemented
valor em vez de False
é a coisa certa a fazer, mesmo para as classes de novo estilo, se a comutatividade dos operadores ==
e !=
for desejada quando os operandos forem de tipos não relacionados (sem herança).
Já estamos lá? Nem tanto. Quantos números únicos temos?
len(set([n1, n2, n3])) # 3 -- oops
Os conjuntos usam os hashes dos objetos e, por padrão, o Python retorna o hash do identificador do objeto. Vamos tentar substituí-lo:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
O resultado final é semelhante a este (adicionei algumas afirmações no final para validação):
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 a identidade do objeto da comparação de valores.