Por que `se None .__ eq __ (“ a ”)` parece ser avaliado como True (mas não exatamente)?

146

Se você executar a seguinte instrução no Python 3.7, ela será impressa (pelos meus testes) b:

if None.__eq__("a"):
    print("b")

No entanto, None.__eq__("a")avalia como NotImplemented.

Naturalmente, "a".__eq__("a")avalia Truee "b".__eq__("a")avalia como False.

Inicialmente, descobri isso ao testar o valor de retorno de uma função, mas não retornei nada no segundo caso - portanto, a função retornou None.

O que está acontecendo aqui?

O arquiteto de IA
fonte

Respostas:

178

Este é um ótimo exemplo de por que os __dunder__métodos não devem ser usados ​​diretamente, pois geralmente não são substituições apropriadas para seus operadores equivalentes; em ==vez disso, você deve usar o operador para comparações de igualdade ou, nesse caso especial, ao verificar None, use is(pule para a parte inferior da resposta para obter mais informações).

Você fez

None.__eq__('a')
# NotImplemented

Que retorna NotImplementeddesde que os tipos sendo comparados são diferentes. Considere outro exemplo em que dois objetos com tipos diferentes estão sendo comparados dessa maneira, como 1e 'a'. Fazer (1).__eq__('a')também não está correto e retornará NotImplemented. A maneira correta de comparar esses dois valores para a igualdade seria

1 == 'a'
# False

O que acontece aqui é

  1. Primeiro, (1).__eq__('a')é tentado, que retorna NotImplemented. Isso indica que a operação não é suportada, portanto
  2. 'a'.__eq__(1)é chamado, que também retorna o mesmo NotImplemented. Assim,
  3. Os objetos são tratados como se não fossem os mesmos e Falsesão retornados.

Aqui está um ótimo MCVE usando algumas classes personalizadas para ilustrar como isso acontece:

class A:
    def __eq__(self, other):
        print('A.__eq__')
        return NotImplemented

class B:
    def __eq__(self, other):
        print('B.__eq__')
        return NotImplemented

class C:
    def __eq__(self, other):
        print('C.__eq__')
        return True

a = A()
b = B()
c = C()

print(a == b)
# A.__eq__
# B.__eq__
# False

print(a == c)
# A.__eq__
# C.__eq__
# True

print(c == a)
# C.__eq__
# True

Obviamente, isso não explica por que a operação retorna verdadeira. Isso ocorre porque, NotImplementedna verdade, é um valor verdadeiro:

bool(None.__eq__("a"))
# True

Igual a,

bool(NotImplemented)
# True

Para obter mais informações sobre quais valores são considerados verdadeiros e falsos, consulte a seção de documentos em Teste do valor da verdade , bem como esta resposta . Vale a pena notar aqui que NotImplementedé verdade, mas teria sido uma história diferente se a classe definisse um método __bool__ou __len__que retornasse Falseou 0respectivamente.


Se você deseja o equivalente funcional do ==operador, use operator.eq:

import operator
operator.eq(1, 'a')
# False

No entanto, conforme mencionado anteriormente, para este cenário específico , em que você está procurando None, use is:

var = 'a'
var is None
# False

var2 = None
var2 is None
# True

O equivalente funcional disso está usando operator.is_:

operator.is_(var2, None)
# True

Noneé um objeto especial e apenas existe uma versão na memória a qualquer momento. IOW, é o único singleton da NoneTypeclasse (mas o mesmo objeto pode ter qualquer número de referências). As diretrizes do PEP8 explicitam isso:

As comparações com singletons como Nonesempre devem ser feitas com isou is not, nunca com os operadores de igualdade.

Em resumo, para singletons como None, uma verificação de referência isé mais apropriada, embora ambas ==e isfuncionem perfeitamente.

cs95
fonte
33

O resultado que você está vendo é causado pelo fato de que

None.__eq__("a") # evaluates to NotImplemented

avalia como NotImplementede NotImplementedo valor de verdade está documentado como sendo True:

https://docs.python.org/3/library/constants.html

Valor especial que deve ser devolvido pelos métodos especiais binários (por exemplo __eq__(), __lt__(), __add__(), __rsub__(), etc.) para indicar que a operação não é implementado em relação ao outro tipo; podem ser devolvidos pelos métodos in-place binários especiais (por exemplo __imul__(), __iand__(), etc.) para a mesma finalidade. Seu valor de verdade é verdadeiro.

Se você chamar o __eq()__método manualmente, em vez de apenas usá- ==lo, precisará estar preparado para lidar com a possibilidade de retorno NotImplementede que seu valor verdadeiro é verdadeiro.

Mark Meyer
fonte
16

Como você já imaginou, None.__eq__("a")avalia se, NotImplementedno entanto, se você tentar algo como

if NotImplemented:
    print("Yes")
else:
    print("No")

o resultado é

sim

isso significa que o valor de verdade de NotImplemented true

Portanto, o resultado da pergunta é óbvio:

None.__eq__(something) rendimentos NotImplemented

E bool(NotImplemented)avalia como True

Então if None.__eq__("a")sempre é verdade

Kanjiu
fonte
1

Por quê?

Retorna a NotImplemented, sim:

>>> None.__eq__('a')
NotImplemented
>>> 

Mas se você olhar para isso:

>>> bool(NotImplemented)
True
>>> 

NotImplementedé, na verdade, um valor verdadeiro, e é por isso que ele retorna b, qualquer coisa que seja Trueaprovada, qualquer coisa que Falsenão seja.

Como resolver isso?

Você precisa verificar se é True, portanto, seja mais desconfiado, como pode ver:

>>> NotImplemented == True
False
>>> 

Então você faria:

>>> if None.__eq__('a') == True:
    print('b')


>>> 

E como você vê, não retornaria nada.

Sub-10
fonte
1
resposta visualmente mais clara - v adição útil - obrigado
scharfmn
1
:) “adição de valor” não chega a capturar o que eu estava tentando dizer (como você OBV ver) - talvez “excelência tardia” é o que eu queria - aplausos
scharfmn
@scharfmn yes? Estou curioso para saber o que você acha que essa resposta acrescenta e que ainda não foi abordada antes.
#
de alguma forma as coisas / repl visuais aqui adicionar clareza - demonstração completa
scharfmn
@scharfmn ... Qual a resposta aceita também embora as solicitações tenham sido removidas. Você votou apenas porque os avisos do terminal foram deixados preguiçosamente?
cs95