Dlaczego argument „if None .__ eq __ („ a ”)„ wydaje się być oceniany na True (ale nie do końca)?

146

Jeśli wykonasz następującą instrukcję w Pythonie 3.7, wydrukuje się ona (z moich testów) b:

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

Jednak None.__eq__("a")szacuje się do NotImplemented.

Oczywiście "a".__eq__("a")ocenia do Truei "b".__eq__("a")ocenia do False.

Odkryłem to początkowo podczas testowania zwracanej wartości funkcji, ale w drugim przypadku nic nie zwróciłem - więc funkcja zwróciła None.

Co tu się dzieje?

Architekt AI
źródło

Odpowiedzi:

178

Jest to doskonały przykład na to, dlaczego __dunder__metod nie należy używać bezpośrednio, ponieważ często nie stanowią one odpowiednich zamienników dla ich równoważnych operatorów; ==zamiast tego powinieneś używać operatora do porównań równości lub w tym szczególnym przypadku, podczas sprawdzania None, użyj is(przejdź do końca odpowiedzi, aby uzyskać więcej informacji).

Zrobiłeś

None.__eq__('a')
# NotImplemented

Który zwraca, NotImplementedponieważ porównywane typy są różne. Rozważmy inny przykład, w którym dwa obiekty o różnych typach są porównywane w ten sposób, na przykład 1i 'a'. Postępowanie (1).__eq__('a')również nie jest poprawne i wróci NotImplemented. Właściwym sposobem porównania tych dwóch wartości dla równości byłby

1 == 'a'
# False

To, co się tutaj dzieje, jest

  1. Najpierw (1).__eq__('a')próbowano, co wraca NotImplemented. Oznacza to, że operacja nie jest obsługiwana, więc
  2. 'a'.__eq__(1)jest wywoływana, co również zwraca to samo NotImplemented. Więc,
  3. Obiekty są traktowane tak, jakby nie były takie same i Falsesą zwracane.

Oto ładny mały MCVE wykorzystujący kilka niestandardowych klas, aby zilustrować, jak to się dzieje:

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

Oczywiście to nie wyjaśnia, dlaczego operacja zwraca prawdę. Dzieje się tak, ponieważ NotImplementedjest to prawdziwa wartość:

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

Taki sam jak,

bool(NotImplemented)
# True

Aby uzyskać więcej informacji na temat tego, jakie wartości są uważane za prawdziwe, a jakie fałszywe, zobacz sekcję z dokumentacją dotyczącą testowania wartości prawdy , a także tę odpowiedź . Warto w tym miejscu zauważyć, że NotImplementedto prawda, ale byłaby to inna historia, gdyby klasa zdefiniowała metodę __bool__lub __len__, która zwraca Falselub 0odpowiednio.


Jeśli chcesz funkcjonalnego odpowiednika ==operatora, użyj operator.eq:

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

Jednak, jak wspomniano wcześniej, w tym konkretnym scenariuszu , w którym szukasz None, użyj is:

var = 'a'
var is None
# False

var2 = None
var2 is None
# True

Funkcjonalnym odpowiednikiem tego jest użycie operator.is_:

operator.is_(var2, None)
# True

Nonejest obiektem specjalnym iw dowolnym momencie w pamięci istnieje tylko 1 wersja. IOW, jest to jedyny singleton w NoneTypeklasie (ale ten sam obiekt może mieć dowolną liczbę odniesień). Te wytyczne PEP8 zrobić to wyraźne:

Porównania do singletonów, takich jak, Nonepowinny być zawsze wykonywane z operatorami równości islub is notnigdy.

Podsumowując, w przypadku singletonów, takich jak Nonesprawdzenie referencji, isjest bardziej odpowiednie, chociaż oba ==i isbędą działać dobrze.

cs95
źródło
33

Wynik, który widzisz, jest spowodowany tym faktem

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

ocenia się na NotImplemented, a NotImplementedwartość prawdy jest udokumentowana jako True:

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

Specjalna wartość, która powinna zostać zwrócona przez binarnych metod specjalnych (np __eq__(), __lt__(), __add__(), __rsub__(), itd.), Aby wskazać, że operacja nie jest realizowane w stosunku do innego rodzaju; mogą być zwracane przez binarne specjalnych metod w miejscu (na przykład __imul__(), __iand__()etc.) dla tego samego celu. Jego wartość prawdziwa jest prawdziwa.

Jeśli wywołujesz __eq()__metodę ręcznie, a nie tylko używasz ==, musisz być przygotowany na to, że może ona powrócić NotImplementedi że jej prawdziwa wartość jest prawdziwa.

Mark Meyer
źródło
16

Jak już się zorientowałeś, None.__eq__("a")ocenia się NotImplementedjednak, jeśli spróbujesz czegoś takiego

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

wynik to

tak

oznacza to, że wartość prawda NotImplemented true

Dlatego wynik pytania jest oczywisty:

None.__eq__(something) plony NotImplemented

I bool(NotImplemented)ocenia się na True

Tak if None.__eq__("a")jest zawsze prawda

Kanjiu
źródło
1

Czemu?

Zwraca NotImplemented, tak:

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

Ale jeśli spojrzysz na to:

>>> bool(NotImplemented)
True
>>> 

NotImplementedjest w rzeczywistości prawdziwą wartością, dlatego zwraca b, wszystko, co jest True, minie, wszystko, Falseco nie.

Jak to rozwiązać?

Musisz sprawdzić, czy tak jest True, więc bądź bardziej podejrzliwy, jak widzisz:

>>> NotImplemented == True
False
>>> 

Więc zrobiłbyś:

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


>>> 

I jak widzisz, nic nie zwróci.

U10-Forward
źródło
1
najbardziej przejrzysta wizualnie odpowiedź - v warty uwagi dodatek - dziękuję
scharfmn
1
:) „wartościowy dodatek” nie do końca oddaje to, co próbowałem powiedzieć (jak widać) - być może „spóźniona doskonałość” jest tym, czego chciałem
pozdrawiam
@scharfmn yes? Ciekawi mnie, co według ciebie dodaje ta odpowiedź, która nie została wcześniej omówiona.
cs95
jakoś wizualnym / Repl rzeczy tutaj dodać klarowność - pełne demo
scharfmn
@scharfmn ... Która zaakceptowana odpowiedź również została usunięta, chociaż monity zostały usunięte. Czy zagłosowałeś tylko dlatego, że monity terminala zostały leniwie pozostawione?
cs95