27

Ran into this problem (in Python 2.7.5) with a little typo:

def foo(): return 3
if foo > 8:
    launch_the_nukes()

Dang it, I accidentally exploded the Moon.

My understanding is that E > F is equivalent to (E).__gt__(F) and for well behaved classes (such as builtins) equivalent to (F).__lt__(E).

If there's no __lt__ or __gt__ operators then I think Python uses __cmp__.

But, none of these methods work with function objects while the < and > operators do work. What goes on under the hood that makes this happen?

>>> foo > 9e9
True
>>> (foo).__gt__(9e9)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute '__gt__'
>>> (9e9).__lt__(foo)
NotImplemented
wim
  • 338,267
  • 99
  • 616
  • 750
Cuadue
  • 3,769
  • 3
  • 24
  • 38
  • 19
    Note that in Python 3.0+, that little typo would give you an obvious `TypeError: unorderable types: function() > int()`. In other words, it's a well-known problem that was solved, and you only have to worry about it if you use old versions of the language. – abarnert Aug 22 '13 at 18:18
  • 1
    @digi_abhshk: Just do it the way the OP did in his code. It's guaranteed to return something consistent each time, but it's not guaranteed _what_ it returns. (In CPython 2.2-2.7, it will effectively compare the types of the objects by name.) – abarnert Aug 22 '13 at 18:21
  • Note that if the left operand's `__gt__` doesn't exist or returns `NotImplemented`, Python tries the right operand's `__lt__`. If that doesn't work, it does weird crap you shouldn't rely on. (It doesn't try `__le__` or `__ge__`, mostly due to NumPy vectorized comparisons.) – user2357112 Aug 22 '13 at 18:21
  • Looks like (1).__lt__(foo) **returns** `NotImplemented`, rather than raising it. Of course `bool(NotImplemented) == True` – Cuadue Aug 22 '13 at 18:22
  • By the way: Surely you mean just `F.__lt__(E)`, not `F.__lt__(E) or F.__eq__(E)`; otherwise you'd be getting the equivalent of `E >= F` – abarnert Aug 22 '13 at 18:27
  • @abarnert the inverse of `>` is `<=` correct? – Cuadue Aug 22 '13 at 18:28
  • 1
    @Cuadue: Yes, but this isn't about the inverse. Drop the abstraction and think about a simple case: `3 > 3` is not true, even though `3 <= 3` is, right? If you wanted to use the inverse here, you'd say `E < F iff not E >= F`, not `E < F iff F >= E`. – abarnert Aug 22 '13 at 18:31
  • possible duplicate of [How does python compare functions?](http://stackoverflow.com/questions/7942346/how-does-python-compare-functions) – soulcheck Aug 22 '13 at 18:42
  • 1
    @Cuadue: With many (but not all) operator special methods, returning `NotImplemented` (or, in C operator slots, returning a special value—IIRC 0, -1, -2, or 2 are all used in different places) means "go to the next fallback to implement this operator". This is documented for `__lt__` [here](http://docs.python.org/2/reference/datamodel.html#object.__lt__). – abarnert Aug 22 '13 at 18:49

3 Answers3

17

But, none of these methods work with function objects while the < and > operators do work. What goes on under the hood that makes this happen?

In default of any other sensible comparison, CPython in the 2.x series compares based on type name. (This is documented as an implementation detail, although there are some interesting exceptions which can only be found in the source.) In the 3.x series this will result in an exception.

The Python spec places some specific constraint on the behaviour in 2.x; comparison by type name is not the only permitted behaviour, and other implementations may do something else. It is not something to be relied on.

abarnert
  • 354,177
  • 51
  • 601
  • 671
Marcin
  • 48,559
  • 18
  • 128
  • 201
  • 1
    Very strange. It looks like `` is actually *less than* ``. So it looks like `type(foo) > type(8)` evaluates to `False`! – Cuadue Aug 22 '13 at 18:27
  • 1
    @Cuadue: Yes, the `function` and `int` types are both the same type, so they're compared by the `type.__lt__` slot-wrapper. – abarnert Aug 22 '13 at 18:28
  • 1
    But 'function' is greater than 'int', which is what this anwser claims. – tdelaney Aug 22 '13 at 18:29
  • @abarnert It seems self inconsistent. `((type(1)).__gt__(type(foo))) == (1 > foo)` evaluates to `False` – Cuadue Aug 22 '13 at 18:32
  • @tdelaney: When compared as `type` instances, the `function` type is greater than the `int` type. But the name `"function"` is less than the name `"int"`, and when you compare a function to an int, it compares _based on type name_, not _based on type object_. – abarnert Aug 22 '13 at 18:33
  • @abarnert Sorry I missed that subtelty, but `((type(1).__name__).__gt__(type(foo).__name__)) == (1 > foo)` is *still* `False`! – Cuadue Aug 22 '13 at 18:34
  • @Cuadue - okay, I gotta admit that's interesting – tdelaney Aug 22 '13 at 18:35
  • 2
    @Cuadue: Ah, that's a _different_ subtlety, which isn't documented, which I hoped wouldn't come up. The names that are being compared aren't actually the ones you see from their `__name__` attribute, and that's probably what's happening here. To be honest, I couldn't guarantee that without looking up the source. Which might be interesting here… – abarnert Aug 22 '13 at 18:36
  • 2
    I think it's [`default_3way_compare`](http://hg.python.org/cpython/file/2.7/Objects/object.c#l762) that implements the fallback code. And it's a bit surprising: objects that don't pass `PyNumber_Check` use the `tp_name` slot of their type object… but objects that _do_ are all treated as if they had the name `""` instead. (And if _both_ sides are numbers, they're compared by id of the type object. This also happens for two different types with the same name.) – abarnert Aug 22 '13 at 18:43
  • 2
    I edited the answer to add links to the docs (which give the "by type name" rule) and the source (which shows that it's not absolutely followed); hopefully anyone interested in full details will read the comments. It might be worth writing up the whole comparison process (as implemented by CPython 2.7) from special attribute lookup to the same-named-type fallback in the fallback as pseudocode for those who don't want to trace through the source… but I'm too lazy to do that… – abarnert Aug 22 '13 at 18:44
  • 2
    There's yet another twist in here. It goes through the same fallbacks you'd expect from the docs, just like two user-defined classes would (see [here](http://pastebin.com/xzMKt1af) for some code that shows the order), except that `int.__cmp__` exists and raises a `TypeError`—which would propagate as an exception for a user-defined (new-style) class, but for a builtin it still works as in Python 1.5, which means it falls back to the default-compare code instead. – abarnert Aug 22 '13 at 18:59
  • @abarnert Thanks for this research. I suspect this makes the answer, taken with your comments and edits the most complete source on this topic. – Marcin Aug 22 '13 at 19:25
  • @Marcin Can you please undermine that statement with a few examples on the Python command line? – Lorenz Lo Sauer Sep 07 '13 at 16:03
  • @losauer I don't know what you mean – Marcin Sep 07 '13 at 20:29
4

For future readers, I am posting this answer because @wim has placed a bounty on this question asserting @Marcin's answer is erroneous with the reasoning that function < int will evaluate to False, and not True as would be expected if lexicographically ordered by type names.

The following answer should clarify some exceptions to the CPython implementation; however, it is only relevant for Python 2.x, since this comparison now throws an exception in Python 3.x+.

The Comparison Algorithm

Python's comparison algorithm is very intricate; when two types are incompatible for comparison using the type's built-in comparison function, it internally defaults to several different functions in an attempt to find a consistent ordering; the relevant one for this question is default_3way_compare(PyObject *v, PyObject *w).

The implementation for default_3way_compare performs the comparison (using lexicographical ordering) on the type's object names instead of their actual values (e.g. if types a and b are not compatible in a < b, it analogously performs type(a).__name__ < type(b).__name__ internally in the C code).

However, there are a few exceptions that do not abide by this general rule:

  • None: Always considered to be smaller (i.e. less) than any other value (excluding other None's of course, as they are are all the same instance).

  • Numeric types (e.g. int, float, etc): Any type that returns a non-zero value from PyNumber_Check (also documented here) will have their type's name resolved to the empty string "" instead of their actual type name (e.g. "int", "float", etc). This entails that numeric types are ordered before any other type (excluding NoneType). This does not appear to apply to the complex type.

    For example, when comparing a numeric type with a function with the statement 3 < foo(), the comparison internally resolves to a string comparison of "" < "function", which is True, despite that the expected general-case resolution "int" < "function" is actually False because of lexicographical ordering. This additional behavior is what prompted the aforementioned bounty, as it defies the expected lexicographical ordering of type names.

See the following REPL output for some interesting behavior:

>>> sorted([3, None, foo, len, list, 3.5, 1.5])
[None, 1.5, 3, 3.5, <built-in function len>, <function foo at 0x7f07578782d0>, <type 'list'>]

More example (in Python 2.7.17)

from pprint import pprint
def foo(): return 3
class Bar(float): pass
bar = Bar(1.5)
pprint(map(
    lambda x: (x, type(x).__name__), 
    sorted(
        [3, None, foo, len, list, -0.5, 0.5, True, False, bar]
    )
))

output:

[(None, 'NoneType'),
 (-0.5, 'float'),
 (False, 'bool'),
 (0.5, 'float'),
 (True, 'bool'),
 (1.5, 'Bar'),
 (3, 'int'),
 (<built-in function len>, 'builtin_function_or_method'),
 (<function foo at 0x10c692e50>, 'function'),
 (<type 'list'>, 'type')]

Additional Insight

Python's comparison algorithm is implemented within Object/object.c's source code and invokes do_cmp(PyObject *v, PyObject *w) for two objects being compared. Each PyObject instance has a reference to its built-in PyTypeObject type through py_object->ob_type. PyTypeObject "instances" are able to specify a tp_compare comparison function that evaluates ordering for two objects of the same given PyTypeObject; for example, int's comparison function is registered here and implemented here. However, this comparison system does not support defining additional behavior between various incompatible types.

Python bridges this gap by implementing its own comparison algorithm for incompatible object types, implemented at do_cmp(PyObject *v, PyObject *w). There are three different attempts to compare types instead of using the object's tp_compare implementation: try_rich_to_3way_compare, try_3way_compare, and finally default_3way_compare (the implementation where we see this interesting behavior in this question).

Hieast
  • 87
  • 5
concision
  • 6,029
  • 11
  • 29
  • @wim You should give this answer the bounty. FYI the information about exceptions is treated briefly in my answer with a link to the source. – Marcin Oct 19 '20 at 18:44
  • The motivation for this design was that numbers be grouped together and ordered when sorting heterogeneous lists. Then an exception came for complex type, and the original intention was no longer convincing. Details on the historical context [here](https://stackoverflow.com/questions/2384078/why-is-0-true-in-python-2). – wim Oct 19 '20 at 19:07
-2

Note: this only works in Python 2.x. In Python 3.x, it gives:

TypeError: '(comparison operator)' not supported between instances of 'function' and 'int'.

In Python 2.x, functions are more than infinitely large. For example, in Python 2.7.16 (repl.it), type this:

> def func():
...    return 0
...
> print(func)
<function func at 0xLOCATION>
> int(func)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string or a number, not 'function'
> id(func)
IDNUMBER
> x=id(func)
> func<x
False
> func==x
False
> inf=float("inf")
> func<inf
False

This shows that 'func' is greater than positive infinity in Python 2.x. Try this in Python 3.8.2 (repl.it):

> def func():
...    return 0
...
> func<10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'function' and 'int'

This shows that comparing with a function as an operand is only supported in Python 2.x, and that when comparing a function in Python 2.x, it is greater than Python's infinity.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Lakshya Raj
  • 1,669
  • 3
  • 10
  • 34