Почему `if None.__eq__(a)` оценивается как True (но не совсем)?

Если вы выполните следующий оператор в Python 3.7, он (из моего тестирования) напечатает b:

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

Однако None.__eq__("a") оценивается как NotImplemented.

Естественно, "a".__eq__("a") оценивается как True, а "b".__eq__("a") оценивается как False.

Сначала я обнаружил это при тестировании возвращаемого значения функции, но во втором случае ничего не возвращал — так что функция вернула None.

Что тут происходит?


person The AI Architect    schedule 31.12.2018    source источник


Ответы (4)


Это отличный пример того, почему методы __dunder__ не следует использовать напрямую, поскольку они довольно часто не являются подходящей заменой для своих эквивалентных операторов; вместо этого вам следует использовать оператор == для сравнения на равенство или, в этом особом случае, при проверке None используйте is (для получения дополнительной информации перейдите к нижней части ответа).

Вы сделали

None.__eq__('a')
# NotImplemented

Который возвращает NotImplemented, так как сравниваемые типы различны. Рассмотрим другой пример, в котором таким образом сравниваются два объекта разных типов, например 1 и 'a'. Выполнение (1).__eq__('a') также неверно и вернет NotImplemented. Правильным способом сравнения этих двух значений на равенство было бы

1 == 'a'
# False

Что здесь происходит

  1. Сначала пробуется (1).__eq__('a'), который возвращает NotImplemented. Это указывает на то, что операция не поддерживается, поэтому
  2. Вызывается 'a'.__eq__(1), который также возвращает тот же самый NotImplemented. Так,
  3. Объекты обрабатываются так, как будто они не совпадают, и возвращается False.

Вот хороший небольшой MCVE, использующий некоторые пользовательские классы, чтобы проиллюстрировать, как это происходит:

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

Конечно, это не объясняет, почему операция возвращает значение true. Это потому, что NotImplemented на самом деле является истинным значением:

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

Такой же как,

bool(NotImplemented)
# True

Дополнительные сведения о том, какие значения считаются истинными, а какие ложными, см. в разделе документации по адресу Тестирование истинности, а также этот ответ. Здесь стоит отметить, что NotImplemented соответствует действительности, но все было бы иначе, если бы класс определил метод __bool__ или __len__, возвращающий False или 0 соответственно.


Если вам нужен функциональный эквивалент оператора ==, используйте operator.eq. :

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

Однако, как упоминалось ранее, для этого конкретного сценария, когда вы проверяете None, используйте is:

var = 'a'
var is None
# False

var2 = None
var2 is None
# True

Функциональным эквивалентом этого является использование operator.is_:

operator.is_(var2, None)
# True

None — это особый объект, и в любой момент времени в памяти существует только 1 версия. IOW, это единственный синглтон класса NoneType (но один и тот же объект может иметь любое количество ссылок). В рекомендациях PEP8 это четко указано:

Сравнения с синглтонами, такими как None, всегда должны выполняться с помощью is или is not, а не операторов равенства.

Таким образом, для одиночек, таких как None, проверка ссылок с is более уместна, хотя и ==, и is будут работать нормально.

person cs95    schedule 31.12.2018

Результат, который вы видите, вызван тем фактом, что

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

оценивается как NotImplemented, а истинное значение NotImplemented задокументировано как True:

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

Специальное значение, которое должно быть возвращено специальными бинарными методами (например, __eq__(), __lt__(), __add__(), __rsub__() и т. д.), чтобы указать, что операция не реализована по отношению к другому типу; может быть возвращен специальными двоичными методами на месте (например, __imul__(), __iand__() и т. д.) для той же цели. Истинное значение истинно.

Если вы вызываете метод __eq()__ вручную, а не просто используете ==, вы должны быть готовы к тому, что он может вернуть NotImplemented и что его значение истинности равно true.

person Mark    schedule 31.12.2018

Как вы уже поняли, None.__eq__("a") оценивается как NotImplemented, однако, если вы попробуете что-то вроде

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

результат

да

это означает, что истинное значение NotImplemented true

Поэтому исход вопроса очевиден:

None.__eq__(something) дает NotImplemented

И bool(NotImplemented) оценивается как True

Так что if None.__eq__("a") всегда верно

person Kami Kaze    schedule 31.12.2018

Почему?

Он возвращает NotImplemented, да:

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

Но если вы посмотрите на это:

>>> bool(NotImplemented)
True
>>> 

NotImplemented на самом деле является истинным значением, поэтому он возвращает b, все, что равно True, пройдет, а все, что равно False, - нет.

Как это решить?

Вы должны проверить, является ли это True, так что будьте более подозрительны, как видите:

>>> NotImplemented == True
False
>>> 

Итак, вы бы сделали:

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


>>> 

И, как видите, это ничего не вернет.

person U11-Forward    schedule 28.01.2019
comment
самый наглядный ответ - v стоящее дополнение - спасибо - person scharfmn; 29.01.2019
comment
:) «достойное дополнение» не совсем отражает то, что я пытался сказать (как вы видите) — возможно, «запоздалое превосходство» — это то, что я хотел — ура - person scharfmn; 30.01.2019
comment
@scharfmn да? Мне любопытно узнать, что, по вашему мнению, добавляет этот ответ, что еще не было рассмотрено ранее. - person cs95; 02.02.2019
comment
каким-то образом визуальные/реплирующие вещи здесь добавляют ясности - полная демонстрация - person scharfmn; 02.02.2019
comment
@scharfmn ... Который также принят в принятом ответе, хотя подсказки были удалены. Вы проголосовали только потому, что подсказки терминала были лениво оставлены? - person cs95; 29.06.2019
comment
Я вижу, что вы говорите. Возможно? Согласен, паразитирует. Но это помогло мне. Вот почему я прокомментировал, чтобы объяснить. Это дань уважения вашему ответу, за который я проголосовал. - person scharfmn; 29.06.2019