Sunday, January 5, 2014

Easy guide to comparing Python classes to lambda functions

Flirting with TypeError

I was reading Peter Norvig's spelling corrector essay and stumbled on this:
This [code] works because max(None, i) is i for any integer i.
I was all like: wow, Python gonna let this punk get away with that? What's the matter? What's the world coming to?
nigel@codumentary:~$ python
Python 3.2.3 (default, Nov 22 1963, 18:30:12)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> help
Type help() for interactive help, or help(object) for help about object.
>>> max(None, 12)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: int() > NoneType()
That's what the world is coming to: TypeError.

But. There is a huge but.

A Huge But

pnorvig@google:/usr/local/secret-project/popups$ python
Python 2.7.3 (default, Apr 20 2012, 22:39:59)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> # it works
>>> max(None, 12)
12
>>> # there is more
>>> class FarmAnimal(object): pass
>>> max(lambda x: x + 12, FarmAnimal)
<class '__main__.FarmAnimal'>
Yes, you can compare class to lambda function in Python2. Python3 throws an exception.

I think that's a really weird behaviour. I mean Python2 allowing it, not Python3 throwing an exception. What's going on here? Has Guido van Rossum gone completely Brendan Eich? What does Python documentation say?

Read the Docs

To quote the docs:
Objects of different types, except different numeric types and different string types, never compare equal; such objects are ordered consistently but arbitrarily (so that sorting a heterogeneous array yields a consistent result)
CPython implementation detail: Objects of different types except numbers are ordered by their type names; objects of the same types that don’t support proper comparison are ordered by their address.
Let's check it:
>>> type(FarmAnimal).__name__
'type'
>>> type(lambda x: x + 12).__name__ 'function'
Type name of FarmAnimal is 'type', type name of lambda x: x + 12 is 'function'. That's why FarmAnimal is greater than lambda x: x + 12 (string 'type' is greater than string 'function').

What about this:
>>> max(None, 12)
12
Is it because string 'int' is greater than string 'NoneType'?
Documentation mentions that numeric types are exception to the rule - not very helpful.

Let's have a look at CPython2 source code.

Read the Source

The source says:
static int
default_3way_compare(PyObject *v, PyObject *w) {    ...    ...     /* None is smaller than anything */     if (v == Py_None)         return -1;     if (w == Py_None)         return 1;     /* different type: compare type names; numbers are smaller */     if (PyNumber_Check(v))         vname = "";     else         vname = v->ob_type->tp_name;     if (PyNumber_Check(w))         wname = "";     else         wname = w->ob_type->tp_name;     c = strcmp(vname, wname);     if (c < 0)         return -1;     if (c > 0)         return 1;     /* Same type name, or (more likely) incomparable numeric types */     return ((Py_uintptr_t)(v->ob_type) < (         Py_uintptr_t)(w->ob_type)) ? -1 : 1; }

Source is good

I was wrong. The answer to question 
Is it because string 'int' is greater than string 'NoneType'? 
is no.

12 is greater than None not because 'int' is greater than 'NoneType'.
12 is greater than None because of this C code:
    /* None is smaller than anything */
    if (v == Py_None)
        return -1;
    if (w == Py_None)
        return 1;
Anything is greater than None in CPython2. None is Python's absolute zero. Here's the proof:
>>> cow = FarmAnimal()
>>> type(cow).__name__
'FarmAnimal'
>>> 'FarmAnimal' > 'NoneType'
False
>>> # Comparing to None ignores type names >>> max(None, cow) <__main__.FarmAnimal object at 0xdeadbeef>

Twitterize it

CPython2 comparison tower is:
1) None
2) Numeric types ordered by type name
3) Non-numeric types ordered by type name


Unleash Hell

And that's why max(None, i) is i for any integer i.

Now you can compare all sorts of stuff in your Python2 programs. Apples to tuples, ashes to exceptions, numbers to None.
Unleash hell!

You should be aware of two things though:
1) Hell won't work in Python3
2) Nobody wants extra troubles while porting code to Python3