Membership testing
Two different instances of float('nan') are not equal to each other. They are "Not a Number" so it makes sense that they shouldn't also have to be equal. They are different instances of objects which are not numbers:
print(float('nan') == float('nan')) # False
As documented here:
For container types such as list, tuple, set, frozenset, dict, or
collections.deque, the expression x in y is equivalent to any(x is e
or x == e for e in y).
There is a checking for identity! that's why you see that behavior in your question and why NaN in a returns True and float('nan') in a doesn't.
Sorting in Python
Python uses the Timsort algorithm for its sorted() function. (Also see this for a textual explanation.) I'm not going to go into that. I just want to demonstrate a simple example:
This is my class A. It's going to be our float('nan') object. It acts like float('nan') in that it returns False for all comparison operations:
class A:
def __init__(self, n):
self.n = n
def __lt__(self, other):
print(self, 'lt is calling', other)
return False
def __gt__(self, other):
print(self, 'gt is calling', other)
return False
def __repr__(self):
return f'A({self.n})'
class B:
def __init__(self, n):
self.n = n
def __lt__(self, other):
print(self, 'lt is calling', other)
return False
def __gt__(self, other):
print(self, 'gt is calling', other)
return False
def __repr__(self):
return f'B({self.n})'
When we use the sorted() function (or the .sort() method of a list) without the reverse=True argument, we're asking for the iterable to be sorted in ascending order. To do this, Python tries to call the __lt__ method successively, starting from the second object in the list to see if it is less than its previous object and so on:
lst = [A(1), B(2), A(3), B(4)]
print(sorted(lst))
output :
B(2) lt is calling A(1)
A(3) lt is calling B(2)
B(4) lt is calling A(3)
[A(1), B(2), A(3), B(4)]
Now, switching back to your example:
lst = [3, A(1), 4, 2, A(1), 1]
print(sorted(lst))
output:
A(1) lt is calling 3
A(1) gt is calling 4
A(1) gt is calling 2
A(1) lt is calling 2
A(1) lt is calling 4
A(1) gt is calling 1
[3, A(1), 1, 2, 4, A(1)]
A(1).__lt__(3) will return False. This means A(1) is not less
than 3 or This means 3 is in correct position relative to A(1).
- Then here
int.__lt__(4, A(1)) gets called and because it returns
NotImplemented object, Python checks to see if A(1) has
implemented __gt__ and yes, so A(1).__gt__(4) will return
False again and this means the A(1) object is in correct place
relative to 4.
- (Etc.)
This is why the result of sorted() seems to be weird, but it's predictable. A(1) object in both cases, I mean when int class returns NotImplemented and when __lt__ gets called from A(1), will return False.
It's better to check the Timsort algorithm and consider those points. I would include the remaining steps if I read Timsort algorithm carefully.