Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom chained comparisons

Tags:

python

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

like image 645
acdr Avatar asked May 10 '16 14:05

acdr


People also ask

What is chained comparison in Python?

Python supports chaining of comparison operators, which means if we wanted to find out if b lies between a and c we can do a < b < c , making code super-intuitive. Python evaluates such expressions like how we do in mathematics. which means a < b < c is evaluated as (a < b) and (b < c) .

Can you chain comparison operators in Javascript?

Javascript does not support the chained comparison syntax used in mathematics: 1 < 2 < 3 // 1 is less than 2 which is less than 3.

What are comparison operators in Python?

A comparison operator in python, also called python relational operator, compares the values of two operands and returns True or False based on whether the condition is met.


2 Answers

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

like image 135
AKS Avatar answered Sep 28 '22 02:09

AKS


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])
like image 35
iulian Avatar answered Sep 28 '22 03:09

iulian