Inconsistent object comparison behavior when inheriting from dict

This problem arose as a result of a failed test, which failed locally and failed on our CI server.

It turned out that some rather tricky comparison of objects was unintentionally made.

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

This test case can probably be simplified further, but this is what I have:

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 its local launch 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 CSR Travis server, this 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 is the comparison behavior for Python, and why does it exhibit this behavior on two installations of the same version?

My initial thought was some kind of id comparison, but looking at the id value, they do not correlate at all with the comparison results.

Update:

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

Update 2:

I just found that I can simplify the test case even with the __int__ and __gt__ , but if I remove any of these methods, the strange behavior will disappear.

+7
python comparison-operators
source share
2 answers

After further study and based on the fantastic answer of @BrenBarn, I found the root of the weird behavior.

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

+1
source share

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

Objects of different types, except for different numeric types and different types of strings, never compare the same; such objects are ordered sequentially but arbitrarily

In other words, dicts are specifically defined to allow comparison with other types, but the result of such comparisons is undefined. (This has been modified in Python 3 so that these kinds of cross-type comparisons are no longer allowed.)

When you override only some of the comparison operators for your type, you complicate 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 comparison behavior (undefined). This way 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 exact pattern of the results you see, but in the end your class relies on undefined behavior, so you cannot expect any consistency compared to this type. Two Python implementations can do something different at some secret implementation level, which creates different undefined behavior. The point of undefined behavior is that you should not know how it works (or you can start relying on it).

By the way, total_ordering is not-op here, and the behavior should be the same if you delete it. total_ordering only adds comparison operators that are not yet defined, but dict already defines them all, so total_ordering do nothing. If you want to create your own ordering relationship in a subclass of a type that already defines its own comparison behavior (for example, dict), then you need to manually override each individual comparison operator.

+5
source share

All Articles