acdr
acdr

Reputation: 4726

Custom chained comparisons

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).

Upvotes: 17

Views: 617

Answers (3)

AKS
AKS

Reputation: 19861

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]).

Upvotes: 8

iulian
iulian

Reputation: 5822

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])

Upvotes: 3

Matthias
Matthias

Reputation: 13232

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])

Upvotes: 1

Related Questions