Python, devo implementar o operador __ne __ () com base em __eq__?

98

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?

Falmarri
fonte

Respostas:

57

Sim, está perfeitamente bem. Na verdade, a documentação pede que você defina __ne__quando define __eq__:

Não há relacionamentos implícitos entre os operadores de comparação. A verdade de x==ynã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.

Em muitos casos (como este), será tão simples quanto negar o resultado de __eq__, mas nem sempre.

Daniel DiPaolo
fonte
12
esta é a resposta certa (aqui, por @ aaron-hall). A documentação que você citou não o incentiva a implementar __ne__usando __eq__, apenas que você o implemente.
Guyarad
2
@guyarad: Na verdade, a resposta de Aaron ainda está um pouco errada graças a não delegação adequada; em vez de tratar um NotImplementedretorno 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 .
ShadowRanger
1
A crítica de ShadowRanger só se aplica a casos muito patológicos (IMHO) e é totalmente abordada em minha resposta abaixo.
Aaron Hall
1
As documentações mais recentes (para 3.7 pelo menos, pode ser ainda anterior) __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.
bluesummers
132

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==ynão implica que x!=yseja 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 NotImplementedpara o tipo verificado, o Python irá verificar apropriadamente other.__eq__(self) na documentação :

O NotImplementedobjeto

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 othermétodo 's primeiro (inversa para <, <=, >=e >). Se NotImplementedfor 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.cemobject_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 NotImplementedem __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_pythonestá 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 == otherdo __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 NotImplementednão significa que seja incorreto. Em vez disso, lidamos com a priorização por NotImplementedmeio da verificação de igualdade com ==. Supondo que ==esteja implementado corretamente, pronto.

not self == othercostumava 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 ise 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 NotImplementedem __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.

Aaron Hall
fonte
3
Excelentes exemplos! Parte da surpresa é que a ordem dos operandos não importa , ao contrário de alguns métodos mágicos com seus reflexos do "lado direito". Para reiterar a parte que perdi (e que me custou muito tempo): O método de comparação avançado da subclasse é tentado primeiro, independentemente de o código ter a superclasse ou a subclasse à esquerda do operador. É por isso que você a1 != c2retornou False--- não foi executado a1.__ne__, mas c2.__ne__, o que negou o método do mixin __eq__ . Já que NotImplementedé verdade, not NotImplementedé False.
Kevin J. Chase
2
Suas atualizações recentes demonstram com sucesso a vantagem de desempenho do 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 isso not (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 .
ShadowRanger
1
O exemplo específico é meio sem importância, na verdade. O problema é que, em sua implementação, o comportamento de seus __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 retornar NotImplemented, 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).
ShadowRanger
1
@AaronHall: Ao reexaminar isso hoje, não acho que sua implementação seja problemática para subclasses normalmente (seria extremamente complicado quebrá-la, e a subclasse, presumida como tendo conhecimento total do pai, deve ser capaz de evitá-la ) Mas acabei de dar um exemplo não complicado em minha resposta. O caso não patológico é o ORM de SQLAlchemy, onde nem __eq__nem __ne__retorna Trueou False, 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).
ShadowRanger
1
Para ser claro, em 99% (ou talvez 99,999%) dos casos, sua solução é boa e (obviamente) mais rápida. Mas uma vez que você não tem controle sobre os casos em que não está bem, como um escritor de biblioteca cujo código pode ser usado por outros (leia-se: qualquer coisa, exceto scripts e módulos simples apenas para uso pessoal), você deve use a implementação correta para cumprir o contrato geral para sobrecarga do operador e trabalhar com qualquer outro código que você possa encontrar. Felizmente, no Py3, nada disso importa, pois você pode omitir __ne__totalmente. Daqui a um ano, Py2 estará morto e nós ignoramos isso. :-)
ShadowRanger
10

Apenas para registro, um portátil Py2 / Py3 canonicamente correto e cruzado __ne__seria parecido com:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Isso funciona com qualquer um que __eq__você definir:

  • Ao contrário 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 de noton __eq__(por exemplo, SQLAlchemy's ORM, onde ambos __eq__e __ne__retornam objetos proxy especiais, não Trueou False, e tentando noto resultado de __eq__retornaria False, em vez do objeto proxy correto).
  • Ao contrário not self.__eq__(other), este delega corretamente ao __ne__da outra instância quando self.__eq__retorna NotImplemented( not self.__eq__(other)seria extra errado, porque NotImplementedé verdade, então quando __eq__não soubesse como fazer a comparação, __ne__retornaria False, 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 usa NotImplementedretornos, isso funciona (com sobrecarga sem sentido), se usa NotImplementedàs vezes, trata-o corretamente. E a verificação de versão do Python significa que, se a classe tiver import-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:

  1. (Aplica-se a todos os operadores) Ao executar LHS OP RHS, tente LHS.__op__(RHS), e se retornar NotImplemented, tente RHS.__rop__(LHS). Exceção: se RHSfor uma subclasse da LHSclasse de, teste RHS.__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ão RHS.__ne__(LHS), invertida se RHSfor uma subclasse da LHSclasse de)
  2. Além da ideia do operador "trocado", não há relação implícita entre os operadores. Mesmo por exemplo da mesma classe, o LHS.__eq__(RHS)retorno Truenão implica em LHS.__ne__(RHS)retornos False(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:

  1. Se você souber como implementar a operação por conta própria, faça-o usando apenas seu próprio conhecimento de como fazer a comparação (nunca delegue, implícita ou explicitamente, para o outro lado da operação; isso corre o risco de incorreção e / ou recursão infinita, dependendo de como você faz isso)
  2. Se você não sabe como implementar a operação sozinho, sempre retorne NotImplemented, para que o Python possa delegar à implementação do outro operando

O problema com not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

nunca delega para o outro lado (e está incorreto se __eq__retorna corretamente NotImplemented). Quando self.__eq__(other)retorna NotImplemented(que é "verdadeiro"), você retorna silenciosamente False, então A() != something_A_knows_nothing_aboutretorna False, quando deveria ter verificado se something_A_knows_nothing_aboutsabia como comparar às instâncias de A, e se não, deveria ter retornado True(já que se nenhum dos lados souber como comparados uns com os outros, eles são considerados diferentes entre si). Se A.__eq__estiver implementado incorretamente (retornando em Falsevez de NotImplementedquando não reconhecer o outro lado), então isso é "correto" da Aperspectiva de, retornando True(uma vez Aque não pensa que é igual, então não é igual), mas pode ser errado desomething_A_knows_nothing_aboutperspectiva de, uma vez que nunca perguntou something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutacaba True, mas something_A_knows_nothing_about != A()poderia False, ou qualquer outro valor de retorno.

O problema com not self == other

def __ne__(self, other):
    return 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__. Mas not self == otherquebra 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 retorna Falsepara todas as comparações, portanto, A() == Incomparable()e A() != Incomparable()ambos retornam False. Com uma implementação correta de A.__ne__(aquela que retorna NotImplementedquando 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__retorna NotImplemented, depois Incomparable.__ne__retorna False, enquanto no último, Incomparable.__ne__retorna Falsediretamente). Mas quando A.__ne__é implementado como return not self == other, A() != Incomparable()retorna True(porque A.__eq__retorna, não NotImplemented, Incomparable.__eq__retorna Falsee o A.__ne__inverte para True), enquanto Incomparable() != A()retornaFalse.

Você pode ver um exemplo disso em ação aqui .

Obviamente, uma aula que sempre retorna Falsepara os dois __eq__e __ne__é um pouco estranha. Mas como mencionei antes, __eq__e __ne__nem precisa retornar True/ False; o SQLAlchemy ORM tem classes com comparadores que retornam um objeto proxy especial para construção de consulta, não True/ Falseem 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:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

funcionará (assumindo que SQLAlchemy saiba como inserir MyClassWithBadNEem uma string SQL; isso pode ser feito com adaptadores de tipo sem a MyClassWithBadNEnecessidade de cooperar), passando o objeto proxy esperado para filter, enquanto:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

vai acabar passando filterum plain False, porque self == otherretorna um objeto proxy, e not self == otherapenas converte o objeto proxy verdadeiro para False. Esperançosamente, filterlança uma exceção ao ser tratado com argumentos inválidos como False. Embora eu tenha certeza de que muitos irão argumentar que MyTable.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, embora return not self == otherapenas funcione em um arranjo.

ShadowRanger
fonte
1
A única resposta correta, completa e honesta (desculpe @AaronHall). Esta deve ser a resposta aceita.
Maggyero
4

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):

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

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 valor NotImplementednos 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 == otherdo __ne__método de Aaron Hall está incorreta, pois remove a simetria do !=operador. Na verdade, ele nunca pode retornar NotImplemented( 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 == othercostumava 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 :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Esses são os chamados métodos de “comparação rica”. A correspondência entre os símbolos do operador e os nomes dos métodos é a seguinte: x<ychamadas x.__lt__(y), x<=ychamadas x.__le__(y), x==ychamadas x.__eq__(y), x!=ychamadas x.__ne__(y), x>ychamadas x.__gt__(y)e x>=y chamadas x.__ge__(y).

Um método de comparação avançado pode retornar o singleton NotImplementedse não implementar a operação para um determinado par de argumentos.

Não há versões de argumentos trocados desses métodos (a serem usados ​​quando o argumento esquerdo não suporta a operação, mas o argumento direito sim); antes, __lt__()e __gt__()são o reflexo um do outro, __le__()e __ge__()são o reflexo um do outro e __eq__()e __ne__()são seu próprio reflexo. Se os operandos são de tipos diferentes e o tipo do operando direito é uma subclasse direta ou indireta do tipo do operando esquerdo, o método refletido do operando direito tem prioridade, caso contrário, o método do operando esquerdo tem prioridade. A subclasse virtual não é considerada.

Traduzir isso para o código Python dá (usando operator_eqfor ==, operator_nefor !=, operator_ltfor <, operator_gtfor >, operator_lefor <=e operator_gefor >=):

def operator_eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__eq__(left)

        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)

        if result is NotImplemented:
            result = right.__eq__(left)

    if result is NotImplemented:
        result = left is right

    return result


def operator_ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ne__(left)

        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)

        if result is NotImplemented:
            result = right.__ne__(left)

    if result is NotImplemented:
        result = left is not right

    return result


def operator_lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__gt__(left)

        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)

        if result is NotImplemented:
            result = right.__gt__(left)

    if result is NotImplemented:
        raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__lt__(left)

        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)

        if result is NotImplemented:
            result = right.__lt__(left)

    if result is NotImplemented:
        raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ge__(left)

        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)

        if result is NotImplemented:
            result = right.__ge__(left)

    if result is NotImplemented:
        raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__le__(left)

        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)

        if result is NotImplemented:
            result = right.__le__(left)

    if result is NotImplemented:
        raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result

Implementação padrão dos métodos de comparação

A documentação adiciona:

Por padrão, __ne__()delega __eq__()e inverte o resultado, a menos que seja NotImplemented. Não há outros relacionamentos implícitos entre os operadores de comparação, por exemplo, a verdade de (x<y or x==y)não implica x<=y.

A implementação padrão dos métodos de comparação ( __eq__, __ne__, __lt__, __gt__, __le__e __ge__) pode, portanto, ser dado por:

def __eq__(self, other):
    return NotImplemented

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

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 retorna NotImplemented, seu inverso not NotImplementedé False(como bool(NotImplemented)está True) em vez do desejado NotImplemented.

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 da not self == otherimplementação em dois casos:

  • o __eq__método retorna NotImplemented;
  • o __eq__método retorna um valor diferente de NotImplemented.

Implementação padrão

Vamos ver o que acontece quando o A.__ne__método usa a implementação padrão e o A.__eq__método retorna NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. !=chamadas A.__ne__.
  2. A.__ne__chamadas A.__eq__.
  3. A.__eq__retorna NotImplemented.
  4. !=chamadas B.__ne__.
  5. B.__ne__retorna "B.__ne__".

Isso mostra que quando o A.__eq__método retorna NotImplemented, o A.__ne__método retorna ao B.__ne__método.

Agora vamos ver o que acontece quando o A.__ne__método usa a implementação padrão e o A.__eq__método retorna um valor diferente de NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=chamadas A.__ne__.
  2. A.__ne__chamadas A.__eq__.
  3. A.__eq__retorna True.
  4. !=retorna not True, isso é False.

Isso mostra que, neste caso, o A.__ne__método retorna o inverso do A.__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ção

Vamos ver o que acontece ao substituir a implementação padrão do A.__ne__método pela not self == otherimplementação e os A.__eq__retornos do método NotImplemented:

class A:

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. !=chamadas A.__ne__.
  2. A.__ne__chamadas ==.
  3. ==chamadas A.__eq__.
  4. A.__eq__retorna NotImplemented.
  5. ==chamadas B.__eq__.
  6. B.__eq__retorna NotImplemented.
  7. ==retorna A() is B(), isso é False.
  8. A.__ne__retorna not False, isso é True.

A implementação padrão do __ne__método retornou "B.__ne__", não True.

Agora vamos ver o que acontece ao substituir a implementação padrão do A.__ne__método pela not self == otherimplementação e o A.__eq__método retorna um valor diferente de NotImplemented:

class A:

    def __eq__(self, other):
        return True

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=chamadas A.__ne__.
  2. A.__ne__chamadas ==.
  3. ==chamadas A.__eq__.
  4. A.__eq__retorna True.
  5. A.__ne__retorna not True, isso é False.

A implementação padrão do __ne__método também retornou Falseneste 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 retorna NotImplemented, ela está incorreta.

Maggyero
fonte
Para o seu último exemplo: "Como esta implementação falha em replicar o comportamento da implementação padrão do __ne__método quando o __eq__método retorna NotImplemented, ela está incorreta." - Adefine igualdade incondicional. Assim A() == 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.
Aaron Hall
@AaronHall Da referência da linguagem Python : "Um método de comparação avançado pode retornar o singleton NotImplementedse não implementar a operação para um determinado par de argumentos. Por convenção, Falsee Truesã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. "
Maggyero
@AaronHall Sua implementação de __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 do NotImplementedvalor.
Maggyero
O exemplo final tem duas classes B,, que retorna uma string verdadeira em todas as verificações de __ne__e Aque retorna Trueem todas as verificações de __eq__. Esta é uma contradição patológica. Sob tal contradição, seria melhor levantar uma exceção. Sem conhecimento de B, Anão tem obrigação de respeitar Ba implementação de __ne__para fins de simetria. Nesse ponto do exemplo, como os Aimplementos __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ê.
Aaron Hall
@AaronHall Para um exemplo mais realista, veja o exemplo SQLAlchemy fornecido por @ShadowRanger. Observe também que o fato de sua implementação __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 ...
Maggyero
-1

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;))

Karl Knechtel
fonte
12
O __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.
Don O'Donnell
8
Ou, alternativamente, se você estiver no Python 2.7 ou 3.x, o decorador functools.total_ordering também é bastante útil.
Adam Parkin
Obrigado pelo aviso. No entanto, passei a perceber muitas coisas nesse sentido no último ano e meio. ;)
Karl Knechtel