Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistent object comparison behaviour when inheriting from dict

This problem arose from a failing test that refused to fail locally, and would only fail on our CI server.

It turned out some rather dodgy object comparison was being unintentionally done.

I'm now rather curious as to why the behavior is so different between two installations of the same Python version (2.7.9).

This test case could probably be simplified further, but this is what I've got:

import operator


class Thing(dict):
    def __int__(self, number):
        return self['number']

    def __gt__(self, other):
        return self['number'] > other

thing = Thing({'number': 2})

for o in [
        operator.lt,
        operator.le,
        operator.eq,
        operator.ne,
        operator.ge,
        operator.gt]:
    print o
    print o(0.01, thing)
    print o(thing, 0.01)

And the result of running it locally is:

<built-in function lt>
True
False
<built-in function le>
True
False
<built-in function eq>
False
False
<built-in function ne>
True
True
<built-in function ge>
False
True
<built-in function gt>
False
True

But on the Travis CI server it is:

<built-in function lt>
True
True
<built-in function le>
False
True
<built-in function eq>
False
False
<built-in function ne>
True
True
<built-in function ge>
True
False
<built-in function gt>
True
True

What kind of comparison behavior is Python falling back to, and why would it exhibit such different behavior on two installations of the same version?

My initial thought was some kind of id based comparison, but from looking at the value of the id, they don't correlate at all with the results of the comparisons.

Update:

This differing behavior only happens when the class inherits from dict. When it inherits from object, the comparisons behave the same on both installations, and give the same results as the local result above.

Update 2:

I've just found that I can simplify the test case even further with just the __int__ and the __gt__ methods, but if I remove either of those methods then the strange behavior disappears.

like image 303
Acorn Avatar asked Mar 05 '15 20:03

Acorn


2 Answers

As mentioned in comments, dict already defines all the comparison operators. The documented behavior is:

Objects of different types, except different numeric types and different string types, never compare equal; such objects are ordered consistently but arbitrarily

In other words, dicts are specifically defined to allow comparisons with other types, but for the result of such comparisons to be undefined. (This was changed in Python 3 so that these sorts of inter-type comparisons are no longer allowed.)

When you override just some of the comparison operators for your type, you complicate things even more. Since your type defines __gt__ but not __lt__, thing > 0.01 will use your custom __gt__, but thing < 0.01 will use the default (undefined) comparison behavior. So you get a type that sometimes uses a deterministic rule, and sometimes gives undefined behavior, depending on which comparison operators you use. I don't know why you see the precise pattern of results you're seeing, but the bottom line is that your class relies on undefined behavior, so you can't expect any consistency in comparisons using this type. The two implementations of Python could be doing something differently at some arcane implementation level that produces different undefined behavior. The point of undefined behavior is you aren't supposed to know how it works (or you might start relying on it).

Incidentally, total_ordering here is a no-op, and the behavior should be the same if you remove it. total_ordering only adds comparison operators that aren't already defined, but dict already defines all of them, so total_ordering won't do anything. If you want to make your own ordering relation on a subclass of a type that already defines its own comparison behavior (like dict), then you need to manually override every individual comparison operator.

like image 72
BrenBarn Avatar answered Oct 06 '22 00:10

BrenBarn


After further investigation, and based on @BrenBarn's fantastic answer I've found the root of the strange behaviour.

The last resort step of the "undefined" comparison is to compare the memory location of the object types. After comparing id(type(thing)) and id(type(0.02)) locally and on the CI server, I see that Thing's id is always higher locally, and always lower on the CI server!

like image 30
Acorn Avatar answered Oct 06 '22 01:10

Acorn