¿Por qué `if None .__ eq __ (“ a ”)` parece evaluar a True (pero no del todo)?

146

Si ejecuta la siguiente declaración en Python 3.7, se imprimirá (según mis pruebas) b:

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

Sin embargo, None.__eq__("a")evalúa a NotImplemented.

Naturalmente, "a".__eq__("a")evalúa Truey "b".__eq__("a")evalúa a False.

Inicialmente descubrí esto cuando probé el valor de retorno de una función, pero no devolví nada en el segundo caso, por lo que la función regresó None.

¿Que está pasando aqui?

El arquitecto de IA
fuente

Respuestas:

178

Este es un gran ejemplo de por qué los __dunder__métodos no deben usarse directamente, ya que a menudo no son reemplazos apropiados para sus operadores equivalentes; en su lugar, debe usar el ==operador para las comparaciones de igualdad, o en este caso especial, al verificar None, usar is(pase al final de la respuesta para obtener más información).

Has hecho

None.__eq__('a')
# NotImplemented

Que regresa NotImplementedya que los tipos que se comparan son diferentes. Considere otro ejemplo en el que dos objetos con diferentes tipos se comparan de esta manera, como 1y 'a'. Hacer (1).__eq__('a')tampoco es correcto y volverá NotImplemented. La forma correcta de comparar estos dos valores para la igualdad sería

1 == 'a'
# False

Lo que pasa aquí es

  1. Primero, (1).__eq__('a')se prueba, que vuelve NotImplemented. Esto indica que la operación no es compatible, por lo que
  2. 'a'.__eq__(1)se llama, que también devuelve lo mismo NotImplemented. Entonces,
  3. Los objetos se tratan como si no fueran lo mismo y Falsese devuelven.

Aquí hay un pequeño MCVE que usa algunas clases personalizadas para ilustrar cómo sucede esto:

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

Por supuesto, eso no explica por qué la operación devuelve verdadero. Esto se debe a NotImplementedque en realidad es un valor verdadero:

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

Igual que,

bool(NotImplemented)
# True

Para obtener más información sobre qué valores se consideran verdadero y falso, consulte la sección de documentos sobre Pruebas de valor de verdad , así como esta respuesta . Vale la pena señalar aquí que NotImplementedes verdad, pero habría sido una historia diferente si la clase hubiera definido un método __bool__o un __len__método que regresó Falseo 0respectivamente.


Si desea el equivalente funcional del ==operador, use operator.eq:

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

Sin embargo, como se mencionó anteriormente, para este escenario específico , donde está buscando None, use is:

var = 'a'
var is None
# False

var2 = None
var2 is None
# True

El equivalente funcional de esto es usar operator.is_:

operator.is_(var2, None)
# True

Nonees un objeto especial, y solo existe 1 versión en la memoria en cualquier momento. IOW, es el único singleton de la NoneTypeclase (pero el mismo objeto puede tener cualquier número de referencias). Las pautas PEP8 hacen esto explícito:

Las comparaciones con singletons como Nonesiempre deben hacerse con iso is not, nunca con los operadores de igualdad.

En resumen, para singletons como None, una verificación de referencia con ises más apropiada, aunque ambas ==y isfuncionarán bien.

cs95
fuente
33

El resultado que está viendo es causado por el hecho de que

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

evalúa a NotImplemented, y NotImplementedel valor de verdad está documentado como True:

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

Valor especial que debe ser devuelto por los métodos especiales binarios (por ejemplo __eq__(), __lt__(), __add__(), __rsub__(), etc.) para indicar que la operación no se lleva a cabo con respecto al otro tipo; puede ser devuelto por los métodos especiales binarios in situ (p __imul__(). ej . __iand__(), etc.) con el mismo propósito. Su valor de verdad es verdadero.

Si llama al __eq()__método manualmente en lugar de solo usarlo ==, debe estar preparado para lidiar con la posibilidad de que regrese NotImplementedy que su valor de verdad sea verdadero.

Mark Meyer
fuente
16

Sin embargo, como ya se imaginó, se None.__eq__("a")evalúa NotImplementedsi intenta algo como

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

el resultado es

si

Esto significa que el valor de verdad de NotImplemented true

Por lo tanto, el resultado de la pregunta es obvio:

None.__eq__(something) rendimientos NotImplemented

Y se bool(NotImplemented)evalúa como verdadero

Entonces if None.__eq__("a")siempre es cierto

Kanjiu
fuente
1

¿Por qué?

Devuelve un NotImplemented, sí:

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

Pero si miras esto:

>>> bool(NotImplemented)
True
>>> 

NotImplementeden realidad es un valor verdadero, por eso regresa b, cualquier cosa que Truepase pasará, cualquier cosa que no pase False.

¿Cómo resolverlo?

Tienes que comprobar si es Trueasí, así que sospecha más, como ves:

>>> NotImplemented == True
False
>>> 

Entonces harías:

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


>>> 

Y como ves, no devolvería nada.

U10-Adelante
fuente
1
la respuesta más clara visualmente - v además vale la pena - gracias
scharfmn
1
:) "la adición que vale la pena" no capta lo que estaba tratando de decir (como veis) - quizás "excelencia tardía" es lo que quería - aplausos
scharfmn
@scharfmn sí? Tengo curiosidad por saber qué cree que agrega esta respuesta que no se ha cubierto antes.
cs95
de alguna manera las cosas visuales / de respuesta aquí agregan claridad - demostración completa
scharfmn
@scharfmn ... que la respuesta aceptada también ha sido eliminada. ¿Votaste únicamente porque las indicaciones de la terminal se han dejado perezosamente?
cs95