17

Python allows expressions like x > y > z, which, according to the docs, is equivalent to (x > y) and (y > z) except y is only evaluated once. (https://docs.python.org/3/reference/expressions.html)

However, this seems to break if I customize comparison functions. E.g. suppose I have the following class: (Apologies for the large block, but once you read the __eq__ method, the rest is trivial.)

class CompareList(list):
    def __repr__(self):
        return "CompareList([" + ",".join(str(x) for x in self) + "])"

    def __eq__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] == other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x == other for x in self)

    def __ne__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] != other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x != other for x in self)

    def __gt__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] > other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x > other for x in self)

    def __ge__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] >= other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x >= other for x in self)

    def __lt__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] < other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x < other for x in self)

    def __le__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] <= other[idx] for idx in xrange(len(self)))
        else:
            return CompareList(x <= other for x in self)

Now I can do fun stuff like CompareList([10, 5]) > CompareList([5, 10]) and it will correctly return CompareList([True,False])

However, chaining these operations doesn't work nicely:

low = CompareList([1])
high = CompareList([2])
print(low > high > low) # returns CompareList([True])

Why not? What happens under the hood here? I know it isn't equivalent to (low > high) > low = (False > low) (because that would return False). It could be low > (high > low) but that wouldn't make sense in terms of operator precedence (normally left-to-right).

2
  • 1
    Check out functools.total_ordering - if I recall correctly, you only need to supply one cmp eg __eq__ and one ordering, eg __lt__ and add @functools.total_ordering to the class, and it should fill in the rest for you. Commented May 10, 2016 at 14:30
  • @dwanderson I remember reading Python would do the rest for you. Commented May 10, 2016 at 19:18

3 Answers 3

8

Python allows expressions like x > y > z, which, according to the docs, is equivalent to (x > y) and (y > z) except y is only evaluated once.

According to this, low > high > low will be equivalent to (low > high) and (high > low).

>>> x = low > high   # CompareList([False])
>>> y = high > low   # CompareList([True]) 
>>> x and y
CompareList([True])

More from the documentation on x and y:

x and y: if x is false, then x, else y

In the above case:

>>> x is False
False
>>> x if x is False else y     # x and y
CompareList([True])

so when you do x and y it returns the y which is CompareList([True]).

Sign up to request clarification or add additional context in comments.

4 Comments

That... is a surprisingly simple answer!
Shouldn't it be "if x is falsy" with the code being either if x or if bool(x) is False? For example '' and True will return '' despite '' is not False. Part of the issue here is that bool(custom_object) will return True unless __nonzero__ is overridden, but filtering in the way OP intended is impossible, as noted in @iulian's answer.
'' and True returns '' because bool('') is False. so if you go by the logic '' if '' is False else True then '' is False evaluates to True and you get ''.
In the above case also, CompareList is already inheriting from builtin list. And according to docs on evaluating the bool(some_obj), _ if the __bool__ is not defined then __len__ will be called. Since it is a list and it has one element which is False it will always return the length 1 and hence will never evaluate to False.
3

The other answers are right, but I wanted to address the actual lack of implementation for this problem, because, as I believe, what the OP would like to get as a result from low > high > low is a CompareList([False]).

Indeed, the low > high > low evaluates to (low > high) and (high > low) and since CompareList([False]) is False evaluates to False (which means that it is True), then the second operand of and operator gets evaluated and returned (as it also evaluates to True).

The key to implementing the chained comparison is to override the and boolean operator along __gt__ and __lt__.

Unfortunately, there is no way to do this, and probably won't be. The PEP 335 - Overloadable Boolean Operators proposal was rejected by Guido, but he might consider making chained comparisons like a < b < c overloadable [1].

Unless that moment, there is no way to get your example to work as expected when using chained comparisons.

The only way to achieve the correct result is by overriding the __and__ method and writing your comparisons like this:

def CompareList(list):
    ...
    def __and__(self, other):
        if isinstance(other, list):
            return CompareList(self[idx] and other[idx] for idx in range(len(self)))
        else:
            return CompareList(x and other for x in self)

Then, by writing in the form below, you'll get the correct answer:

low = CompareList([1, 2])
high = CompareList([2, 2])
print((low >= high) & (high >= low)) # returns CompareList([False, True])

2 Comments

Not sure why this isn't higher up, very insightful, especially the PEP reference. I would note that CompareList([False]) is False should probably be changed to bool(CompareList([False])) is False since its truthiness is being checked, not its identity.
@JaredGoguen Thank you! I've posted it 1 hour after other answers were posted, so that's why...
1

You should return a boolean value from your comparison methods.

To cite the documentation for "rich comparison" methods:

By convention, False and True are returned for a successful comparison. However, these methods can return any value, so if the comparison operator is used in a Boolean context (e.g., in the condition of an if statement), Python will call bool() on the value to determine if the result is true or false.

To break it down for this case:

exp1 = low > high
print(exp1)
print(bool(exp1))
exp2 = high > low
print(exp2)
print(bool(exp2))

Will give you

CompareList([False])
True
CompareList([True])
True

Now we do the last operation and print out the result

print(exp1 and exp2)

Because both values evaluate to True you'll get

CompareList([True])

2 Comments

I don't see how that has a relevance to the question of chaining comparisons. I'm aware that saying if CompareList([True, False]): doesn't make sense.
I added some more explanations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.