Tenho uma aula em que desejo substituir o __eq__()
operador. Parece fazer sentido que eu deva substituir o __ne__()
operador também, mas faz sentido implementar com __ne__
base em __eq__
como tal?
class A:
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
Ou há algo que estou perdendo na maneira como o Python usa esses operadores que torna isso não uma boa ideia?
python
comparison
operators
python-datamodel
Falmarri
fonte
fonte
__ne__
usando__eq__
, apenas que você o implemente.NotImplemented
retorno de um lado como uma dica para delegar__ne__
do outro lado,not self == other
é (assumindo que o operando__eq__
não sabe como comparar o outro operando) delegando implicitamente para__eq__
do outro lado e, em seguida, invertendo-o. Para tipos estranhos, por exemplo, os campos do SQLAlchemy ORM, isso causa problemas .__ne__
delega automaticamente para__eq__
e a citação nesta resposta não existe mais nos documentos. Resumindo, é perfeitamente pitônico apenas implementar__eq__
e deixar__ne__
delegar.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 :
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 EXEMPLOVeja a prova disso
__ne__()
operador com base em__eq__
e__ne__
em Python 2fornece comportamento incorreto na demonstração abaixo.
Resposta longa
A documentação do Python 2 diz:
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:
e na seção "o que há de novo" , vemos que este comportamento mudou:
Para a implementação
__ne__
, preferimos usar o==
operador em vez de usar o__eq__
método diretamente para que, seself.__eq__(other)
uma subclasse retornarNotImplemented
para o tipo verificado, o Python irá verificar apropriadamenteother.__eq__(self)
na documentação :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 oother
método 's primeiro (inversa para<
,<=
,>=
e>
). SeNotImplemented
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 .: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:
Instancie instâncias não equivalentes:
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==
:Essas instâncias, testadas em Python 3, também funcionam corretamente:
E lembre-se de que eles foram
__ne__
implementados com__eq__
- embora este seja o comportamento esperado, a implementação está incorreta:Comportamento inesperado:
Observe que esta comparação contradiz as comparações acima (
not wrong1 == wrong2
).e,
Não pule
__ne__
no Python 2Para evidências de que você não deve pular a implementação
__ne__
no Python 2, consulte estes objetos equivalentes:O resultado acima deve ser
False
!Fonte Python 3
A implementação padrão de CPython para
__ne__
estátypeobject.c
emobject_richcompare
: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 manipuladoNotImplemented
.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çamentoNotImplemented
em__ne__
.Alguém pode assumir incorretamente que
==
pode retornarNotImplemented
.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)E as comparações:
atuação
Não acredite apenas na minha palavra, vamos ver o que tem mais desempenho:
Acho que esses números de desempenho falam por si:
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:
__ne__
Nunca ter voltadoNotImplemented
não significa que seja incorreto. Em vez disso, lidamos com a priorização porNotImplemented
meio da verificação de igualdade com==
. Supondo que==
esteja implementado corretamente, pronto.Bem, vamos explicar isso.
Conforme observado anteriormente, o Python 3 por padrão trata
__ne__
primeiro verificando seself.__eq__(other)
retornaNotImplemented
(um singleton) - que deve ser verificado comis
e retornado se for o caso, caso contrário, deve retornar o inverso. Aqui está essa lógica escrita como um mixin de classes:Isso é necessário para a correção da API Python de nível C e foi introduzido no Python 3, tornando
__ne__
métodos neste patch para fechar o problema 21408 e__ne__
métodos na limpeza subsequente removidos aquiredundante. 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:Então, por essa lógica, para manter a simetria, precisamos escrever o complicado
__ne__
, independente da versão do Python.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:Conclusão
Para código compatível com Python 2, use
==
para implementar__ne__
. É mais: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.
fonte
a1 != c2
retornouFalse
--- não foi executadoa1.__ne__
, masc2.__ne__
, o que negou o método do mixin__eq__
. Já queNotImplemented
é verdade,not NotImplemented
éFalse
.not (self == other)
, mas ninguém está argumentando que não é rápido (bem, mais rápido do que qualquer outra opção no Py2 de qualquer maneira). O problema é que está errado em alguns casos; O próprio Python costumava fazer issonot (self == other)
, mas mudou porque estava incorreto na presença de subclasses arbitrárias . O mais rápido para a resposta errada ainda está errado .__ne__
delegados para__eq__
(de ambos os lados se necessário), mas nunca cai para__ne__
o outro lado mesmo quando ambos__eq__
"desistem". Os__ne__
delegados corretos para si próprios__eq__
, mas se isso retornarNotImplemented
, ele voltará para ir para o outro lado__ne__
, em vez de inverter o do outro lado__eq__
(uma vez que o outro lado pode não ter optado explicitamente por delegar para__eq__
, e você não deveria estar tomando essa decisão).__eq__
nem__ne__
retornaTrue
ouFalse
, mas sim um objeto proxy (que passa a ser "verdadeiro"). Implementar incorretamente__ne__
significa que o pedido é importante para a comparação (você só obtém um proxy em um pedido).__ne__
totalmente. Daqui a um ano, Py2 estará morto e nós ignoramos isso. :-)Apenas para registro, um portátil Py2 / Py3 canonicamente correto e cruzado
__ne__
seria parecido com:Isso funciona com qualquer um que
__eq__
você definir:not (self == other)
, não interfere em alguns casos complicados / complexos envolvendo comparações em que uma das classes envolvidas não implica que o resultado de__ne__
é o mesmo que o resultado denot
on__eq__
(por exemplo, SQLAlchemy's ORM, onde ambos__eq__
e__ne__
retornam objetos proxy especiais, nãoTrue
ouFalse
, e tentandonot
o resultado de__eq__
retornariaFalse
, em vez do objeto proxy correto).not self.__eq__(other)
, este delega corretamente ao__ne__
da outra instância quandoself.__eq__
retornaNotImplemented
(not self.__eq__(other)
seria extra errado, porqueNotImplemented
é verdade, então quando__eq__
não soubesse como fazer a comparação,__ne__
retornariaFalse
, implicando que os dois objetos eram iguais quando na verdade o único objeto solicitado não tinha ideia, o que implicaria em um padrão de não igual)Se você
__eq__
não usaNotImplemented
retornos, isso funciona (com sobrecarga sem sentido), se usaNotImplemented
às vezes, trata-o corretamente. E a verificação de versão do Python significa que, se a classe tiverimport
-ed no Python 3, ela__ne__
será deixada indefinida, permitindo que a__ne__
implementação de fallback nativa e eficiente do Python (uma versão C acima) assuma o controle.Por que isso é necessário
Regras de sobrecarga do Python
A explicação de por que você faz isso em vez de outras soluções é um tanto misteriosa. Python tem algumas regras gerais sobre sobrecarregar operadores, e operadores de comparação em particular:
LHS OP RHS
, tenteLHS.__op__(RHS)
, e se retornarNotImplemented
, tenteRHS.__rop__(LHS)
. Exceção: seRHS
for uma subclasse daLHS
classe de, testeRHS.__rop__(LHS)
primeiro . No caso de operadores de comparação,__eq__
e__ne__
são seus próprios "rop" s (então a ordem de teste para__ne__
éLHS.__ne__(RHS)
, entãoRHS.__ne__(LHS)
, invertida seRHS
for uma subclasse daLHS
classe de)LHS.__eq__(RHS)
retornoTrue
não implica emLHS.__ne__(RHS)
retornosFalse
(na verdade, os operadores nem mesmo precisam retornar valores booleanos; ORMs como SQLAlchemy intencionalmente não o fazem, permitindo uma sintaxe de consulta mais expressiva). A partir do Python 3, a__ne__
implementação padrão se comporta dessa maneira, mas não é contratual; você pode substituir__ne__
de maneiras que não são estritamente opostas__eq__
.Como isso se aplica a comparadores de sobrecarga
Então, quando você sobrecarrega um operador, você tem duas tarefas:
NotImplemented
, para que o Python possa delegar à implementação do outro operandoO problema com
not self.__eq__(other)
nunca delega para o outro lado (e está incorreto se
__eq__
retorna corretamenteNotImplemented
). Quandoself.__eq__(other)
retornaNotImplemented
(que é "verdadeiro"), você retorna silenciosamenteFalse
, entãoA() != something_A_knows_nothing_about
retornaFalse
, quando deveria ter verificado sesomething_A_knows_nothing_about
sabia como comparar às instâncias deA
, e se não, deveria ter retornadoTrue
(já que se nenhum dos lados souber como comparados uns com os outros, eles são considerados diferentes entre si). SeA.__eq__
estiver implementado incorretamente (retornando emFalse
vez deNotImplemented
quando não reconhecer o outro lado), então isso é "correto" daA
perspectiva de, retornandoTrue
(uma vezA
que não pensa que é igual, então não é igual), mas pode ser errado desomething_A_knows_nothing_about
perspectiva de, uma vez que nunca perguntousomething_A_knows_nothing_about
;A() != something_A_knows_nothing_about
acabaTrue
, massomething_A_knows_nothing_about != A()
poderiaFalse
, ou qualquer outro valor de retorno.O problema com
not self == other
é mais sutil. Vai ser correto para 99% das classes, incluindo todas as classes para as quais
__ne__
é o inverso lógico de__eq__
. Masnot self == other
quebra ambas as regras mencionadas acima, o que significa que para classes onde__ne__
não é o inverso lógico de__eq__
, os resultados são mais uma vez não simétricos, porque um dos operandos nunca é perguntado se ele pode implementar de__ne__
alguma forma, mesmo que o outro operando não pode. O exemplo mais simples é uma classe esquisita que retornaFalse
para todas as comparações, portanto,A() == Incomparable()
eA() != Incomparable()
ambos retornamFalse
. Com uma implementação correta deA.__ne__
(aquela que retornaNotImplemented
quando não sabe como fazer a comparação), a relação é simétrica;A() != Incomparable()
eIncomparable() != A()
concordar sobre o resultado (porque no primeiro caso,A.__ne__
retornaNotImplemented
, depoisIncomparable.__ne__
retornaFalse
, enquanto no último,Incomparable.__ne__
retornaFalse
diretamente). Mas quandoA.__ne__
é implementado comoreturn not self == other
,A() != Incomparable()
retornaTrue
(porqueA.__eq__
retorna, nãoNotImplemented
,Incomparable.__eq__
retornaFalse
e oA.__ne__
inverte paraTrue
), enquantoIncomparable() != A()
retornaFalse.
Você pode ver um exemplo disso em ação aqui .
Obviamente, uma aula que sempre retorna
False
para os dois__eq__
e__ne__
é um pouco estranha. Mas como mencionei antes,__eq__
e__ne__
nem precisa retornarTrue
/False
; o SQLAlchemy ORM tem classes com comparadores que retornam um objeto proxy especial para construção de consulta, nãoTrue
/False
em absoluto (eles são "verdadeiros" se avaliados em um contexto booleano, mas nunca devem ser avaliados em tal contexto).Ao não sobrecarga
__ne__
corretamente, você vai quebrar as classes desse tipo, como o código:funcionará (assumindo que SQLAlchemy saiba como inserir
MyClassWithBadNE
em uma string SQL; isso pode ser feito com adaptadores de tipo sem aMyClassWithBadNE
necessidade de cooperar), passando o objeto proxy esperado parafilter
, enquanto:vai acabar passando
filter
um plainFalse
, porqueself == other
retorna um objeto proxy, enot self == other
apenas converte o objeto proxy verdadeiro paraFalse
. Esperançosamente,filter
lança uma exceção ao ser tratado com argumentos inválidos comoFalse
. Embora eu tenha certeza de que muitos irão argumentar queMyTable.fieldname
deve ser consistentemente do lado esquerdo da comparação, o fato é que não há nenhuma razão programática para impor isso no caso geral, e um genérico correto__ne__
funcionará de qualquer maneira, emborareturn not self == other
apenas funcione em um arranjo.fonte
Resposta curta: sim (mas leia a documentação para fazer isso direito)
A implementação do
__ne__
método por ShadowRanger é a correta (e passa a ser a implementação padrão do__ne__
método desde Python 3.4):Por quê? Porque mantém uma propriedade matemática importante, a simetria do
!=
operador. Este operador é binário, então seu resultado deve depender do tipo dinâmico de ambos os operandos, não apenas de um. Isso é implementado por meio de despacho duplo para linguagens de programação que permitem despacho múltiplo (como Julia ). Em Python, que permite apenas despacho único, o despacho duplo é simulado para métodos numéricos e métodos de comparação ricos, retornando o valorNotImplemented
nos métodos de implementação que não suportam o tipo do outro operando; o intérprete tentará então o método refletido do outro operando.A implementação
not self == other
do__ne__
método de Aaron Hall está incorreta, pois remove a simetria do!=
operador. Na verdade, ele nunca pode retornarNotImplemented
(not NotImplemented
éFalse
) e, portanto, o__ne__
método com prioridade mais alta nunca pode recorrer ao__ne__
método com prioridade mais baixa.not self == other
costumava ser a implementação padrão do__ne__
método em Python 3 , mas era um bug que foi corrigido no Python 3.4 em janeiro de 2015, como o ShadowRanger notou (consulte o problema # 21408 ).Implementação dos operadores de comparação
A Referência de linguagem Python para Python 3 declara em seu capítulo III Modelo de dados :
Traduzir isso para o código Python dá (usando
operator_eq
for==
,operator_ne
for!=
,operator_lt
for<
,operator_gt
for>
,operator_le
for<=
eoperator_ge
for>=
):Implementação padrão dos métodos de comparação
A documentação adiciona:
A implementação padrão dos métodos de comparação (
__eq__
,__ne__
,__lt__
,__gt__
,__le__
e__ge__
) pode, portanto, ser dado por:Portanto, esta é a implementação correta do
__ne__
método. E nem sempre retorna o inverso do__eq__
método porque quando o__eq__
método retornaNotImplemented
, seu inversonot NotImplemented
éFalse
(comobool(NotImplemented)
estáTrue
) em vez do desejadoNotImplemented
.Implementações incorretas de
__ne__
Como Aaron Hall demonstrou acima,
not self.__eq__(other)
não é a implementação padrão do__ne__
método. Mas nem énot self == other
. O último é demonstrado abaixo, comparando o comportamento da implementação padrão com o comportamento danot self == other
implementação em dois casos:__eq__
método retornaNotImplemented
;__eq__
método retorna um valor diferente deNotImplemented
.Implementação padrão
Vamos ver o que acontece quando o
A.__ne__
método usa a implementação padrão e oA.__eq__
método retornaNotImplemented
:!=
chamadasA.__ne__
.A.__ne__
chamadasA.__eq__
.A.__eq__
retornaNotImplemented
.!=
chamadasB.__ne__
.B.__ne__
retorna"B.__ne__"
.Isso mostra que quando o
A.__eq__
método retornaNotImplemented
, oA.__ne__
método retorna aoB.__ne__
método.Agora vamos ver o que acontece quando o
A.__ne__
método usa a implementação padrão e oA.__eq__
método retorna um valor diferente deNotImplemented
:!=
chamadasA.__ne__
.A.__ne__
chamadasA.__eq__
.A.__eq__
retornaTrue
.!=
retornanot True
, isso éFalse
.Isso mostra que, neste caso, o
A.__ne__
método retorna o inverso doA.__eq__
método. Assim, o__ne__
método se comporta como anunciado na documentação.Substituir a implementação padrão do
A.__ne__
método pela implementação correta fornecida acima produz os mesmos resultados.not self == other
implementaçãoVamos ver o que acontece ao substituir a implementação padrão do
A.__ne__
método pelanot self == other
implementação e osA.__eq__
retornos do métodoNotImplemented
:!=
chamadasA.__ne__
.A.__ne__
chamadas==
.==
chamadasA.__eq__
.A.__eq__
retornaNotImplemented
.==
chamadasB.__eq__
.B.__eq__
retornaNotImplemented
.==
retornaA() is B()
, isso éFalse
.A.__ne__
retornanot False
, isso éTrue
.A implementação padrão do
__ne__
método retornou"B.__ne__"
, nãoTrue
.Agora vamos ver o que acontece ao substituir a implementação padrão do
A.__ne__
método pelanot self == other
implementação e oA.__eq__
método retorna um valor diferente deNotImplemented
:!=
chamadasA.__ne__
.A.__ne__
chamadas==
.==
chamadasA.__eq__
.A.__eq__
retornaTrue
.A.__ne__
retornanot True
, isso éFalse
.A implementação padrão do
__ne__
método também retornouFalse
neste caso.Uma vez que esta implementação falha em replicar o comportamento da implementação padrão do
__ne__
método quando o__eq__
método retornaNotImplemented
, ela está incorreta.fonte
__ne__
método quando o__eq__
método retorna NotImplemented, ela está incorreta." -A
define igualdade incondicional. AssimA() == B()
,. Portanto,A() != B()
deveria ser falso , e é . Os exemplos dados são patológicos (ou seja__ne__
, não devem retornar uma string e__eq__
não devem depender de__ne__
- ao contrário,__ne__
devem depender de__eq__
, que é a expectativa padrão no Python 3). Eu ainda estou -1 nesta resposta até que você possa mudar minha mente.NotImplemented
se não implementar a operação para um determinado par de argumentos. Por convenção,False
eTrue
são retornados para uma comparação bem-sucedida. No entanto, esses métodos podem retornar qualquer valor , portanto, se o operador de comparação for usado em um contexto booleano (por exemplo, na condição de uma instrução if), o Python chamarábool()
o valor para determinar se o resultado é verdadeiro ou falso. "__ne__
mata uma propriedade matemática importante, a simetria do!=
operador. Este operador é binário, portanto seu resultado deve depender do tipo dinâmico de ambos os operandos, não apenas de um. Isso é implementado corretamente em linguagens de programação por meio de despacho duplo para linguagem que permite despacho múltiplo . Em Python, que permite apenas um despacho único, o despacho duplo é simulado pelo retorno doNotImplemented
valor.B
,, que retorna uma string verdadeira em todas as verificações de__ne__
eA
que retornaTrue
em todas as verificações de__eq__
. Esta é uma contradição patológica. Sob tal contradição, seria melhor levantar uma exceção. Sem conhecimento deB
,A
não tem obrigação de respeitarB
a implementação de__ne__
para fins de simetria. Nesse ponto do exemplo, como osA
implementos__ne__
são irrelevantes para mim. Encontre um caso prático e não patológico para demonstrar seu ponto de vista. Eu atualizei minha resposta para me dirigir a você.__ne__
funcionar em casos de uso típicos não a torna correta. As aeronaves Boeing 737 MAX voaram 500.000 voos antes dos acidentes ...Se todos
__eq__
,__ne__
,__lt__
,__ge__
,__le__
, e__gt__
faz sentido para a classe, em seguida, basta implementar__cmp__
em seu lugar. Caso contrário, faça o que está fazendo, por causa da parte que Daniel DiPaolo disse (enquanto eu estava testando em vez de procurar;))fonte
__cmp__()
método especial não é mais suportado no Python 3.x, portanto, você deve se acostumar a usar os operadores de comparação avançados.