17

Can someone explain me the differences between the two. Are those normally equivalent ? Maybe I'm completely wrong here, but I thought that each comparison operator was necessarily related to one “rich comparison” method. This is from the documentation:

The correspondence between operator symbols and method names is as follows:

x<y calls x.__lt__(y), x<=y calls x.__le__(y), x==y calls x.__eq__(y), x!=y calls x.__ne__(y), x>y calls x.__gt__(y), and x>=y calls x.__ge__(y).

Here is an example that demonstrates my confusion.

Python 3.x:

dict1 = {1:1}
dict2 = {2:2}

>>> dict1 < dict2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'dict' and 'dict'
>>> dict1.__lt__(dict2)
NotImplemented

Python 2.x:

dict1 = {1:1}
dict2 = {2:2}

>>> dict1 < dict2
True
>>> dict1.__lt__(dict2)
NotImplemented

From the python 3 example, it seems logic that calling dict1 < dict2 is not supported. But what about Python 2 example ? Why is it accepted ?

I know that unlike Python 2, in Python 3, not all objects supports comparison operators. At my surprise though, both version return the NotImplemented singleton when calling __lt__().

scharette
  • 9,437
  • 8
  • 33
  • 67
  • On second thought, I misread. Will reopen. – cs95 Dec 13 '17 at 16:11
  • @cᴏʟᴅsᴘᴇᴇᴅ Hmmm, the question is significantly differrent from mine. It does not explain the difference between the two and does not provide any insight on when `NotImplemented` is returned... – scharette Dec 13 '17 at 16:14
  • I also find funny the fact that `<` and `__lt__` do not return the same result. They should be since `<` calls `__lt__`, right? – Ma0 Dec 13 '17 at 16:43
  • @Ev.Kounis Yes, this is my confusion. I always taken for granted that both were almost essentially the same. Can you confirm that it indeed does not return the same thing ? Maybe it is a code problem ? – scharette Dec 13 '17 at 16:48
  • @scharette I can. Tried it with Python 3.6.1. We need to wait for the big guns on this. – Ma0 Dec 13 '17 at 16:50

2 Answers2

11

This is relying on the __cmp__ magic method, which is what the rich-comparison operators were meant to replace:

>>> dict1 = {1:1}
>>> dict2 = {2:2}
>>> dict1.__cmp__
<method-wrapper '__cmp__' of dict object at 0x10f075398>
>>> dict1.__cmp__(dict2)
-1

As to the ordering logic, here is the Python 2.7 documentation:

Mappings (instances of dict) compare equal if and only if they have equal (key, value) pairs. Equality comparison of the keys and values enforces reflexivity.

Outcomes other than equality are resolved consistently, but are not otherwise defined.

With a footnote:

Earlier versions of Python used lexicographic comparison of the sorted (key, value) lists, but this was very expensive for the common case of comparing for equality. An even earlier version of Python compared dictionaries by identity only, but this caused surprises because people expected to be able to test a dictionary for emptiness by comparing it to {}.

And, in Python 3.0, ordering has been simplified. This is from the documentation:

The ordering comparison operators (<, <=, >=, >) raise a TypeError exception when the operands don’t have a meaningful natural ordering.

builtin.sorted() and list.sort() no longer accept the cmp argument providing a comparison function. Use the key argument instead.

The cmp() function should be treated as gone, and the __cmp__() special method is no longer supported. Use __lt__() for sorting, __eq__() with __hash__(), and other rich comparisons as needed. (If you really need the cmp() functionality, you could use the expression (a > b) - (a <> b) as the equivalent for cmp(a, b).)

So, to be explicit, in Python 2, since the rich comparison operators are not implemented, dict objects will fall-back to __cmp__, from the data-model documentation:

object.__cmp__(self, other)
Called by comparison operations if rich comparison (see above) is not defined. Should return a negative integer if self < other, zero if self == other, a positive integer if self > other.

scharette
  • 9,437
  • 8
  • 33
  • 67
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • Thank you for the answer, but it does not seems to answer my above questions. Maybe it is because I don't understand and if it is, I'm sorry. I still don't understand why both implementation is not returning the same value. It is clearly mention that when comparing two objects with the `<`, `__lt__()` is called. This is obviously not the case in my example. – scharette Dec 13 '17 at 17:57
  • 1
    @scharette yes, as explained, in Python 2, `__cmp__` is a fall-back if rich comparison operators are not defined. Your misunderstanding is that `<` is always associated with `__lt__()` which is *not the case* in Python 2 because it was still supporting the legacy `__cmp__` method, which `dict` objects *did use* – juanpa.arrivillaga Dec 13 '17 at 17:59
  • Ok, so for dict, `__lt__` is not defined even in python 2? – scharette Dec 13 '17 at 18:01
  • Yes, I was just confused why does Python 2 and Python 3 did not result in the same output. but from what I understand now, `__cmp__` is not use in Python 3 anymore. – scharette Dec 13 '17 at 18:06
  • Just a suggestion, I would specify that the ordering has been change in python 3 according to this [link](https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons) Thank you so much for leading me there. – scharette Dec 13 '17 at 18:07
  • @scharette: A `dict.__lt__` method exists, for reasons stemming from the C implementation (all rich comparisons map to one C function pointer in CPython, so if a C type has any rich comparison methods, it has to have all of them), but it always returns `NotImplemented`. – user2357112 Dec 13 '17 at 18:08
  • @scharette Python 2 and Python 3 are different languages. Python 2 kept a lot of baggage around from even early versions of Python. Consider the fact that the `<>` operator is still supported in Python 2! Note, rich-comparisons were only added in Python 2.1 I believe, before, *everything* used `__cmp__`. This is similar to why in Python 2 you can still pass a `cmp=` argument to `sorted` instead of `key=`, and Python 3 doesn't support `cmp` – juanpa.arrivillaga Dec 13 '17 at 18:08
  • @juanpa.arrivillaga Why does `OrderedDict` not supporting comparison operators then ? Is it just not implemented or there is another reason? If this is another topic I can ask another question on Stack maybe ? – scharette Dec 13 '17 at 18:50
  • @scharette sounds like another topic. But `OrderedDict` is a mapping, which, as pointed out by the docs, "Outcomes other than equality are resolved consistently, but are not otherwise defined." – juanpa.arrivillaga Dec 13 '17 at 19:00
  • @juanpa.arrivillaga Yes, but this is considering there is no ordering, which is the case for a `dict` object, but not for an `OrderedDict`. I think will ask another question on that subject. Thanks ! – scharette Dec 13 '17 at 19:09
  • @scharette that says nothing about ordered mappings vs unordered mappings, it simply says *mappings*. But then even if you are using an ordered dict, why assume insertion order is how you want to compare things? You can always implement your own way to compare them explicitly. – juanpa.arrivillaga Dec 13 '17 at 19:11
  • @juanpa.arrivillaga I was reffering to python 3 documentation, where it says _The ordering comparison operators (<, <=, >=, >) raise a TypeError exception when the operands don’t have a meaningful natural ordering._ – scharette Dec 13 '17 at 19:12
  • @juanpa.arrivillaga Because, calling `list(1,2,3) == list(3,2,1)` will return `False`, whitout implementing my own way to compare them explicitly. – scharette Dec 13 '17 at 19:17
2

Note for operator < versus __lt__:

import types

class A:
    def __lt__(self, other): return True

def new_lt(self, other): return False

a = A()
print(a < a, a.__lt__(a))  # True True
a.__lt__ = types.MethodType(new_lt, a)
print(a < a, a.__lt__(a))  # True False
A.__lt__ = types.MethodType(new_lt, A)
print(a < a, a.__lt__(a))  # False False

< calls __lt__ defined on class; __lt__ calls __lt__ defined on object.

It's usually the same :) And it is totally delicious to use: A.__lt__ = new_lt

Ivo
  • 21
  • 2