Inconsistent object comparison behaviour when inheriting from dict - python

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.

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.

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!

Related

Where is the default behavior for object equality (`==`) defined?

According to the object.__eq__() documentation, the default (that is, in the object class) implementation for == is as follows:
True if x is y else NotImplemented
Still following the documentation for NotImplemented, I inferred that NotImplemented implies that the Python runtime will try the comparison the other way around. That is try y.__eq__(x) if x.__eq__(y) returns NotImplemented (in the case of the == operator).
Now, the following code prints False and True in python 3.9:
class A:
pass
print(A() == A())
print(bool(NotImplemented))
So my question is the following: where does the documentation mention the special behavior of NotImplemented in the context of __eq__ ?
PS : I found an answer in CPython source code but I guess that this must/should be somewhere in the documentation.
According to the object.__eq__() documentation, the default (that is, in the object class) implementation for == is as follows
No; that is the default implementation of __eq__. ==, being an operator, cannot be implemented in classes.
Python's implementation of operators is cooperative. There is hard-coded logic that uses the dunder methods to figure out what should happen, and possibly falls back on a default. This logic is outside of any class.
You can see another example with the built-in len: a class can return whatever it likes from its __len__ method, and you can in principle call it directly and get a value of any type. However, this does not properly implement the protocol, and len will complain when it doesn't get a positive integer back. There is not any class which contains that type-checking and value-checking logic. It is external.
Still following the documentation for NotImplemented, I inferred that NotImplemented implies that the Python runtime will try the comparison the other way around. That is try y.__eq__(x) if x.__eq__(y) returns NotImplemented (in the case of the == operator).
NotImplemented is just an object. It is not syntax. It does not have any special behavior, and in Python, simply returning a value does not trigger special behavior besides that the value is returned.
The external code for binary operators will try to look for the matching __op__, and try to look for the matching __rop__ if __op__ didn't work. At this point, NotImplemented is not an acceptable answer (it is a sentinel that exists specifically for this purpose, because None is an acceptable answer). In general, if the answer so far is still NotImplemented, then the external code will raise NotImplementedError.
As a special case, objects that don't provide their own comparison (i.e., the default from object is used for __eq__ or __ne__) will compare as "not equal" unless they are identical. The C implementation repeats the identity check (in case a class explicitly defines __eq__ or __ne__ to return NotImplemented directly, I guess). This is because it is considered sensible to give this result, and obnoxious to make == fail all the time when there is a sensible default.
However, the two objects are still not orderable without explicit logic, since there isn't a reasonable default. (You could compare the pointer values, but they're arbitrary and don't have anything to do with the Python logic that got you to that point; so ordering things that way isn't realistically useful for writing Python code.) So, for example, x < y will raise a TypeError if the comparison logic isn't provided. (It does this even if x is y; you could reasonably say that <= and >= should be true in this case, and < and > should be false, but it makes things too complicated and is not very useful.)
[Observation: print(bool(NotImplemented)) prints True]
Well, yes; NotImplemented is an object, so it's truthy by default; and it doesn't represent a numeric value, and isn't a container, so there's no reason for it to be falsy.
However, that also doesn't tell us anything useful. We don't care about the truthiness of NotImplemented here, and it isn't used that way in the Python implementation. It is just a sentinel value.
where does the documentation mention the special behavior of NotImplemented in the context of __eq__ ?
Nowhere, because it isn't a behavior of NotImplemented, as explained above.
Okay, but that leaves underlying question: where does the documentation explain what the == operator does by default?
Answer: because we are talking about an operator, and not about a method, it's not in the section about dunder methods. It's in section 6, which talks about expressions. Specifically, 6.10.1. Value comparisons:
The default behavior for equality comparison (== and !=) is based on the identity of the objects. Hence, equality comparison of instances with the same identity results in equality, and equality comparison of instances with different identities results in inequality. A motivation for this default behavior is the desire that all objects should be reflexive (i.e. x is y implies x == y).

Behavior of NotImpemented in comparison

I recently found out that python has a special value NotImpemented to be used with respect to binary special methods to indicate that some operation has not been implemented.
The peculiar about this is that when checked in a binary situation it is always equivalent to True.
For example using io.BytesIO (which is a case where __eq__ in not implemented for example) for two objects in comparison will virtually return True. As in this example (encoded_jpg_io1 and encoded_jpg_io2 are objects of the io.BytesIO class):
if encoded_jpg_io1.__ne__(encoded_jpg_io2):
print('Equal')
else:
print('Unequal')
Equal
if encoded_jpg_io1.__eq__(encoded_jpg_io2) == True:
print('Equal')
else:
print('Unequal')
Unequal
Since the second style is a bit too verbose and normally not prefered (even my pyCharm suggests to remove the explicit comparison with True) isn't a bit tricky behavior? I wouldn't have noticed it if I haven't explicitly print the result of the Boolean operation (which is not Boolean in this case at all).
I guess suggesting to be considered False would cause the same problem with __ne__ so we arew back to step one.
So, the only way to check out for these cases is by doing an exact comparison with True or False in the opposite case.
I know that NotImpemented is preferred over NotImplementedError for various reasons so I am not asking for any explanation over why this matter.
Per convention, objects that do not define a __bool__ method are considered truthy. From the docs:
By default, an object is considered true unless its class defines either a __bool__() method that returns False or a __len__() method that returns zero
This means that most classes, functions, and other builtin singletons are considered true, since they don't go out of their way to specify different behavior. (An exception is None, which is one of the few built-in singletons that does specifically signal it should be considered false):
>>> bool(int) # the class, not an integer object
True
>>> bool(min)
True
>>> bool(object())
True
>>> bool(...) # that's the Ellipsis object
True
>>> bool(NotImplemented)
True
There is no real reason for the NotImplemented object to break this convention. The problem with your code isn't that NotImplemented is considered truthy; the real problem is that x.__eq__(y) is not equivalent to x == y.
If you want to compare two objects for equality, doing it with x.__eq__(y) is incorrect. Using x.__eq__(y) == True instead is still incorrect.
The correct solution is to do comparison with the == operator. If, for whatever reason, you can't use the == operator directly, you should use the operator.eq function instead.

In python, is there some kind of mapping to return the "False value" of a type?

I am looking for some kind of a mapping function f() that does something similar to this:
f(str) = ''
f(complex) = 0j
f(list) = []
Meaning that it returns an object of type that evaluates to False when cast to bool.
Does such a function exist?
No, there is no such mapping. Not every type of object has a falsy value, and others have more than one. Since the truth value of a class can be customized with the __bool__ method, a class could theoretically have an infinite number of (different) falsy instances.
That said, most builtin types return their falsy value when their constructor is called without arguments:
>>> str()
''
>>> complex()
0j
>>> list()
[]
Nope, and in general, there may be no such value. The Python data model is pretty loose about how the truth-value of a type may be implemented:
object.__bool__(self)
Called to implement truth value testing and the built-in operation
bool(); should return False or True. When this method is not defined,
__len__() is called, if it is defined, and the object is considered true if its result is nonzero. If a class defines neither __len__()
nor __bool__(), all its instances are considered true.
So consider:
import random
class Wacky:
def __bool__(self):
return bool(random.randint(0,1))
What should f(Wacky) return?
This is actually called an identity element, and in programming is most often seen as part of the definition of a monoid. In python, you can get it for a type using the mzero function in the PyMonad package. Haskell calls it mempty.
Not all types have such a value to begin with. Others may have many such values. The most correct way of doing this would be to create a type-to-value dict, because then you could check if a given type was in the dict at all, and you could chose which value is the correct one if there are multiple options. The drawback is of course that you would have to somehow register every type you were interested in.
Alternatively, you could write a function using some heuristics. If you were very careful about what you passed into the function, it would probably be of some limited use. For example, all the cases you show except complex are containers that generalize with cls().
complex actually works like that too, but I mention it separately because int and float do not. So if your attempt with the empty constructor fails by returning a truthy object or raising a TypeError, you can try cls(0). And so on and so forth...
Update
#juanpa.arrivillaga's answer actually suggests a clever workaround that will work for most classes. You can extend the class and forcibly create an instance that will be falsy but otherwise identical to the original class. You have to do this by extending because dunder methods like __bool__ are only ever looked up on the class, never on an instance. There are also many types where such methods can not be replaced on the instance to begin with. As #Aran-Fey's now-deleted comment points out, you can selectively call object.__new__ or t.__new__, depending on whether you are dealing with a very special case (like int) or not:
def f(t):
class tx(t):
def __bool__(self):
return False
try:
return object.__new__(tx)
except TypeError:
return tx.__new__(tx)
This will only work for 99.9% of classes you ever encounter. It is possible to create a contrived case that raises a TypeError when passed to object.__new__ as int does, and does not allow for a no-arg version of t.__new__, but I doubt you will ever find such a thing in nature. See the gist #Aran-Fey made to demonstrate this.
No such function exists because it's not possible in general. A class may have no falsy value or it may require reversing an arbitrarily complex implementation of __bool__.
What you could do by breaking everything else is to construct a new object of that class and forcibly assign its __bool__ function to one that returns False. Though I suspect that you are looking for an object that would otherwise be a valid member of the class.
In any case, this is a Very Bad Idea in classic style.

Testing function object (functor) equality, how is it evaluated? Do I use `is` or `==`?

Consider these functions:
def f():
print("WTF?!")
def g():
print("WTF?!")
They both do exactly the same thing, but a test of f == g still gives False. Do I assume from this that functor equality is evaluated by reference, and that there is no difference between is and ==?
Whether or not that is the case, which one is better to use (even if only stylistically)?
By the way I'm primarily interested in Python 3 (Python 3.6).
EDIT
This question is not a duplicate, I think. I understand the difference between reference equality and value equality, I just want to understand how == uses value equality (if at all) on functors.
Function objects have no custom __eq__ method (this method is called when comparing values with ==) so they fall back to the superclasses __eq__ method. In this case it's object.__eq__ which, indeed, just compares if they are the same object.
So:
>>> f == g
False
is identical (in this case) to:
>>> f is g
False
Just in case your interested how I know that functions have no custom __eq__ method:
>>> type(f).__eq__ is object.__eq__
True
No you cant , because of this :
https://en.wikipedia.org/wiki/Rice%27s_theorem

Python comparison between built-in and user-defined types

How does Python 3 compare a built-in object (on the lhs) to a user-defined object (on the rhs)?
Does the built-in __eq__ method simply delegate the comparison to the rhs (rhs.__eq__(self))?
I didn't find any statement about this in the docs. The docs state:
Objects of different types, except different numeric types, never compare equal.
It's quite misleading because:
class X:
def __eq__(self, rhs)
return True
x = X()
'abc' == x # True
I think the doc statement should be rephrased as follows:
Objects of different built-in types, except different numeric types, never compare equal.
and should furthermore clarify how the comparison to user-defined class instances is performed.
To answer the questions:
How does Python 3 compare a built-in object (on the lhs) to a user-defined object (on the rhs)?
The same way as with any other object comparisons (including None!).
Does the built-in __eq__ method simply delegate the comparison to the rhs (rhs.__eq__(self))?
No. The built-in __eq__ does not delegate like this. There is a higher-construct at work that covers the behavior of == in Python.
Given a == b, where a.__eq__(b) returns NotImplemented then b.__eq__(a) will be invoked and the result of the used as the result of the equality test. (False is returned if both __eq__ implementations return NotImplemented.)
Thus, given x (of class X), and given that "abc".__eq__(x) returns NotImplemented, then x.__eq__("abc") is invoked (and evaluates to True per the question).
The same applies to the other standard comparison operators.
While I don't care to speculate too much on the documentation (or possible mis-wording), I believe it is entirely accurate if taken in context of stdObj.__eq__(obj) as opposed to stdObj == obj.
See also:
The documentation on the __eq__ protocol which briefly mentions NotImplemented
Why return NotImplemented instead of raising NotImplementedError

Categories

Resources