Python, devo implementar o __ne__()
operador com base em __eq__
?
Resposta curta: Não implemente, mas se precisar, use ==
, não__eq__
No Python 3, !=
é a negação de ==
por padrão, então você nem mesmo é obrigado a escrever um __ne__
, e a documentação não é mais opinativa sobre como escrever um.
De um modo geral, para código somente Python 3, não escreva um a menos que você precise ofuscar a implementação pai, por exemplo, para um objeto embutido.
Ou seja, tenha em mente o comentário de Raymond Hettinger :
O __ne__
método segue automaticamente __eq__
apenas se
__ne__
ainda não estiver definido em uma superclasse. Portanto, se você está herdando de um integrado, é melhor substituir ambos.
Se você precisa que seu código funcione em Python 2, siga a recomendação para Python 2 e ele funcionará perfeitamente em Python 3.
No Python 2, o próprio Python não implementa automaticamente nenhuma operação em termos de outra - portanto, você deve definir o __ne__
em termos de em ==
vez de __eq__
. POR EXEMPLO
class A(object):
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self == other # NOT `return not self.__eq__(other)`
Veja a prova disso
- implementar
__ne__()
operador com base em __eq__
e
- não implementando
__ne__
em Python 2
fornece comportamento incorreto na demonstração abaixo.
Resposta longa
A documentação do Python 2 diz:
Não há relacionamentos implícitos entre os operadores de comparação. A verdade de x==y
não implica que x!=y
seja falsa. Assim, ao definir __eq__()
, deve-se também definir de __ne__()
forma que os operadores se comportem conforme o esperado.
Isso significa que se definirmos __ne__
em termos do inverso de __eq__
, podemos obter um comportamento consistente.
Esta seção da documentação foi atualizada para Python 3:
Por padrão, __ne__()
delega __eq__()
e inverte o resultado, a menos que seja NotImplemented
.
e na seção "o que há de novo" , vemos que este comportamento mudou:
!=
agora retorna o oposto de ==
, a menos que ==
retorne NotImplemented
.
Para a implementação __ne__
, preferimos usar o ==
operador em vez de usar o __eq__
método diretamente para que, se self.__eq__(other)
uma subclasse retornar NotImplemented
para o tipo verificado, o Python irá verificar apropriadamente other.__eq__(self)
na documentação :
O NotImplemented
objeto
Este tipo possui um único valor. Existe um único objeto com este valor. Este objeto é acessado por meio do nome embutido
NotImplemented
. Métodos numéricos e métodos de comparação ricos podem retornar esse valor se não implementarem a operação para os operandos fornecidos. (O intérprete tentará então a operação refletida ou algum outro fallback, dependendo do operador.) Seu valor verdadeiro é verdadeiro.
Quando dado um operador de comparação rico, se eles não são do mesmo tipo, cheques Python se o other
é um subtipo, e se ele tem esse operador definido, ele usa o other
método 's primeiro (inversa para <
, <=
, >=
e >
). Se NotImplemented
for retornado, então ele usa o método oposto. (Ele não verifica o mesmo método duas vezes.) O uso do ==
operador permite que essa lógica ocorra.
Expectativas
Semanticamente, você deve implementar __ne__
em termos de verificação de igualdade, pois os usuários de sua classe esperam que as seguintes funções sejam equivalentes para todas as instâncias de A .:
def negation_of_equals(inst1, inst2):
"""always should return same as not_equals(inst1, inst2)"""
return not inst1 == inst2
def not_equals(inst1, inst2):
"""always should return same as negation_of_equals(inst1, inst2)"""
return inst1 != inst2
Ou seja, ambas as funções acima devem sempre retornar o mesmo resultado. Mas isso depende do programador.
Demonstração de comportamento inesperado ao definir com __ne__
base em __eq__
:
Primeiro a configuração:
class BaseEquatable(object):
def __init__(self, x):
self.x = x
def __eq__(self, other):
return isinstance(other, BaseEquatable) and self.x == other.x
class ComparableWrong(BaseEquatable):
def __ne__(self, other):
return not self.__eq__(other)
class ComparableRight(BaseEquatable):
def __ne__(self, other):
return not self == other
class EqMixin(object):
def __eq__(self, other):
"""override Base __eq__ & bounce to other for __eq__, e.g.
if issubclass(type(self), type(other)): # True in this example
"""
return NotImplemented
class ChildComparableWrong(EqMixin, ComparableWrong):
"""__ne__ the wrong way (__eq__ directly)"""
class ChildComparableRight(EqMixin, ComparableRight):
"""__ne__ the right way (uses ==)"""
class ChildComparablePy3(EqMixin, BaseEquatable):
"""No __ne__, only right in Python 3."""
Instancie instâncias não equivalentes:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Comportamento esperado:
(Observação: embora cada segunda asserção de cada uma das opções abaixo seja equivalente e, portanto, logicamente redundante à anterior, estou incluindo-as para demonstrar que a ordem não importa quando uma é uma subclasse da outra. )
Essas instâncias foram __ne__
implementadas com ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Essas instâncias, testadas em Python 3, também funcionam corretamente:
assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1
E lembre-se de que eles foram __ne__
implementados com __eq__
- embora este seja o comportamento esperado, a implementação está incorreta:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Comportamento inesperado:
Observe que esta comparação contradiz as comparações acima ( not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
e,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Não pule __ne__
no Python 2
Para evidências de que você não deve pular a implementação __ne__
no Python 2, consulte estes objetos equivalentes:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
O resultado acima deve ser False
!
Fonte Python 3
A implementação padrão de CPython para __ne__
está typeobject.c
emobject_richcompare
:
case Py_NE:
/* By default, __ne__() delegates to __eq__() and inverts the result,
unless the latter returns NotImplemented. */
if (Py_TYPE(self)->tp_richcompare == NULL) {
res = Py_NotImplemented;
Py_INCREF(res);
break;
}
res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
if (res != NULL && res != Py_NotImplemented) {
int ok = PyObject_IsTrue(res);
Py_DECREF(res);
if (ok < 0)
res = NULL;
else {
if (ok)
res = Py_False;
else
res = Py_True;
Py_INCREF(res);
}
}
break;
Mas o padrão __ne__
usa __eq__
?
Os __ne__
detalhes de implementação padrão do Python 3 no nível C usam __eq__
porque o nível mais alto ==
( PyObject_RichCompare ) seria menos eficiente - e, portanto, também deve ser manipulado NotImplemented
.
Se __eq__
for implementado corretamente, a negação de ==
também está correta - e nos permite evitar detalhes de implementação de baixo nível em nosso __ne__
.
O uso ==
nos permite manter nossa lógica de baixo nível em um só lugar e evitar o endereçamento NotImplemented
em __ne__
.
Alguém pode assumir incorretamente que ==
pode retornar NotImplemented
.
Na verdade, ele usa a mesma lógica da implementação padrão do __eq__
, que verifica a identidade (consulte do_richcompare e nossa evidência abaixo)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
E as comparações:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
atuação
Não acredite apenas na minha palavra, vamos ver o que tem mais desempenho:
class CLevel:
"Use default logic programmed in C"
class HighLevelPython:
def __ne__(self, other):
return not self == other
class LowLevelPython:
def __ne__(self, other):
equal = self.__eq__(other)
if equal is NotImplemented:
return NotImplemented
return not equal
def c_level():
cl = CLevel()
return lambda: cl != cl
def high_level_python():
hlp = HighLevelPython()
return lambda: hlp != hlp
def low_level_python():
llp = LowLevelPython()
return lambda: llp != llp
Acho que esses números de desempenho falam por si:
>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029
Isso faz sentido quando você considera que low_level_python
está fazendo lógica em Python que, de outra forma, seria tratada no nível C.
Resposta a algumas críticas
Outro respondente escreve:
A implementação not self == other
do __ne__
método de Aaron Hall está incorreta, pois ele nunca pode retornar NotImplemented
( not NotImplemented
é False
) e, portanto, o __ne__
método que tem prioridade nunca pode cair no __ne__
método que não tem prioridade.
__ne__
Nunca ter voltado NotImplemented
não significa que seja incorreto. Em vez disso, lidamos com a priorização por NotImplemented
meio da verificação de igualdade com ==
. Supondo que ==
esteja implementado corretamente, pronto.
not self == other
costumava ser a implementação padrão do __ne__
método em Python 3 , mas era um bug e foi corrigido no Python 3.4 em janeiro de 2015, como ShadowRanger notou (consulte o problema # 21408).
Bem, vamos explicar isso.
Conforme observado anteriormente, o Python 3 por padrão trata __ne__
primeiro verificando se self.__eq__(other)
retorna NotImplemented
(um singleton) - que deve ser verificado com is
e retornado se for o caso, caso contrário, deve retornar o inverso. Aqui está essa lógica escrita como um mixin de classes:
class CStyle__ne__:
"""Mixin that provides __ne__ functionality equivalent to
the builtin functionality
"""
def __ne__(self, other):
equal = self.__eq__(other)
if equal is NotImplemented:
return NotImplemented
return not equal
Isso é necessário para a correção da API Python de nível C e foi introduzido no Python 3, tornando
redundante. Todos os __ne__
métodos relevantes foram removidos, incluindo aqueles que implementam sua própria verificação, bem como aqueles que delegam __eq__
diretamente ou via ==
- e ==
era a maneira mais comum de fazer isso.
A simetria é importante?
Nosso crítico persistente fornece um exemplo patológico para fazer o caso para a manipulação NotImplemented
em __ne__
, valorizando simetria acima de tudo. Vamos construir o argumento com um exemplo claro:
class B:
"""
this class has no __eq__ implementation, but asserts
any instance is not equal to any other object
"""
def __ne__(self, other):
return True
class A:
"This class asserts instances are equivalent to all other objects"
def __eq__(self, other):
return True
>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)
Então, por essa lógica, para manter a simetria, precisamos escrever o complicado __ne__
, independente da versão do Python.
class B:
def __ne__(self, other):
return True
class A:
def __eq__(self, other):
return True
def __ne__(self, other):
result = other.__eq__(self)
if result is NotImplemented:
return NotImplemented
return not result
>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)
Aparentemente, não devemos nos importar que essas instâncias são iguais e não iguais.
Eu proponho que a simetria é menos importante do que a presunção de código sensato e seguir o conselho da documentação.
No entanto, se A tivesse uma implementação sensata de __eq__
, então ainda poderíamos seguir minha direção aqui e ainda teríamos simetria:
class B:
def __ne__(self, other):
return True
class A:
def __eq__(self, other):
return False # <- this boolean changed...
>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)
Conclusão
Para código compatível com Python 2, use ==
para implementar __ne__
. É mais:
- corrigir
- simples
- performante
Somente no Python 3, use a negação de baixo nível no nível C - é ainda mais simples e eficiente (embora o programador seja responsável por determinar se está correta ).
Novamente, não escreva lógica de baixo nível em Python de alto nível.
__ne__
usando__eq__
, apenas que você o implemente.