What is the operator precedence when writing a double inequality in Python (explicitly in the code, and how can this be overridden for arrays?)
Asked Answered
C

4

17

What is the specific code, in order, being executed when I ask for something like

>>> 1 <= 3 >= 2
True

If both have equal precedence and it's just the order of their evaluation, why does the second inequality function as (3 >= 2) instead of (True >= 2)

Consider for example the difference between these

>>> (1 < 3) < 2
True

>>> 1 < 3 < 2
False

Is it just a pure syntactical short-cut hard-coded into Python to expand the second as the and of the two statements?

Could I change this behavior for a class, such that a <= b <= c gets expanded to something different? It's looking like the following is the case

a (logical operator) b (logical operator) c 
    --> (a logical operator b) and (b logical operator c)

but the real question is how this gets implemented in code.

I'm curious so that I can replicate this kind of __lt__ and __gt__ behavior in some of my own classes, but I am confused about how this is accomplished holding the middle argument constant.

Here's a specific example:

>>> import numpy as np

>>> tst = np.asarray([1,2,3,4,5,6])

>>> 3 <= tst
array([False, False,  True,  True,  True,  True], dtype=bool)

>>> 3 <= tst <= 5
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/home/ely/<ipython-input-135-ac909818f2b1> in <module>()
----> 1 3 <= tst <= 5

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

It would be nice to override this so that it "just works" with arrays too, like this:

>>> np.logical_and(3 <= tst, tst <= 5)
array([False, False,  True,  True,  True,  False], dtype=bool)

Added for clarification

In the comments it is indicated that I did a poor job of explaining the question. Here's some clarifying remarks:

1) I am not looking for a simple explanation of the fact that the interpreter pops an and in between the two chained inequalities. I already knew that and said so above.

2) For an analogy to what I want to do, consider the with statement (link). The following:

with MyClass(some_obj) as foo:
    do_stuff()

unpacks into

foo = MyClass(some_obj)
foo.__enter__()
try:
    do_stuff()
finally:
    foo.__exit__()

So by writing MyClass appropriately, I can do many special things inside of the with statement.

I am asking whether there is a similar code unpacking of the chained inequality by which I can intercept what it's doing and redirect it to use array-style logical operators instead just for the classes I care about.

I feel this is very clear from my question, especially the example, but hopefully this makes it more clear.

Creepie answered 30/9, 2012 at 2:43 Comment(0)
F
13

I'm not totally sure what you're looking for, but a quick disassembly shows that a < b < c is not compiled to the same bytecode as a < b and b < c

>>> import dis
>>>
>>> def f(a, b, c):
...     return a < b < c
...
>>> dis.dis(f)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 DUP_TOP
              7 ROT_THREE
              8 COMPARE_OP               0 (<)
             11 JUMP_IF_FALSE_OR_POP    21
             14 LOAD_FAST                2 (c)
             17 COMPARE_OP               0 (<)
             20 RETURN_VALUE
        >>   21 ROT_TWO
             22 POP_TOP
             23 RETURN_VALUE
>>>
>>> def f(a, b, c):
...     return a < b and b < c
...
>>> dis.dis(f)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 COMPARE_OP               0 (<)
              9 JUMP_IF_FALSE_OR_POP    21
             12 LOAD_FAST                1 (b)
             15 LOAD_FAST                2 (c)
             18 COMPARE_OP               0 (<)
        >>   21 RETURN_VALUE

Edit 1: Digging further, I think this is something weird or wrong with numpy. Consider this example code, I think it works as you would expect.

class Object(object):
    def __init__(self, values):
        self.values = values
    def __lt__(self, other):
        return [x < other for x in self.values]
    def __gt__(self, other):
        return [x > other for x in self.values]

x = Object([1, 2, 3])
print x < 5 # [True, True, True]
print x > 5 # [False, False, False]
print 0 < x < 5 # [True, True, True]

Edit 2: Actually this doesn't work "properly"...

print 1 < x # [False, True, True]
print x < 3 # [True, True, False]
print 1 < x < 3 # [True, True, False]

I think it's comparing boolean values to numbers in the second comparison of 1 < x < 3.

Edit 3: I don't like the idea of returning non-boolean values from the gt, lt, gte, lte special methods, but it's actually not restricted according to the Python documentation.

http://docs.python.org/reference/datamodel.html#object.lt

By convention, False and True are returned for a successful comparison. However, these methods can return any value...

Finochio answered 1/10, 2012 at 14:23 Comment(6)
This is not quite what I am looking for, but it is helpful and closer to the right track than the other answers so far. Since it doesn't yield the same byte code, the question is what Python functions are being inserted to make up the call in the first case, and how I can override them.Creepie
Yes, you appear to be correct. This is a NumPy bug. For example, if I do the following: list((-1 < vect)[:,0]) and list((vect < 5)[:,0]) then it works. So the and is choking on the fact that it's two arguments are numpy.ndarray instead of list. This is very odd; it means that __gt__ and __lt__ must have some extra ndarray cruft in them.Creepie
It does appear to have issues with getting the right True/False value though, but at least it's not choking on the array types. Lots more digging to do. I wonder if this has anything to do with __and__.Creepie
@EMS, what you want is simply impossible. You should use the binary operators instead which work fine (thats what __and__ is). The and operator cannot do any element wise logic, it is simply not possible in Python, the result is what you find in Edit 2. Only the last comparison/object can be returned, and that will not do what you expect.Monamonachal
I think you're missing my point. I am asking how can I force the double sided inequality to expand into something that doesn't use the regular and.Creepie
@EMS, well you obviously can't, if you look at the compiled code it does exactly the same (ignoring that its a bit optimized), both use the JUMP_IF_FALSE_OP_POP logic which is exactly the and operator.Monamonachal
C
7

Both have the same precedence, but are evaluated from left-to-right according to the documentation. An expression of the form a <= b <= c gets expanded to a <= b and b <= c.

Chalcopyrite answered 30/9, 2012 at 2:45 Comment(7)
Why doesn't it result in (1 <= 3) --> True --> (True >= 2) --> type error?Creepie
The expression a <= b <= c gets expanded to a <= b and b <= cWey
In code, how does the expansion happen. Or is this just a one-off hard-coded Python idiom?Creepie
See my expanded question above.Creepie
I really don't think removing this answer it going to get you better responses. Firstly, that's what voting is for, and secondly, continuing to improve your question will have more impact.Zoophobia
@EMS: what substantive points are you referring to? If you want to get something like a numpy array to "just work" with chained comparisons this way, then you'll need to override __and__. array([0,5]) > 2 and array([2,5]) < 10 both work and produce a bool-dtyped array, but (array([0,5]) > 2) and (array([2,5]) < 10) won't, because that expression is ambiguous and numpy decides to refuse the temptation to guess. You can't tell Python to use something other than and to unchain the comparison.Sulemasulf
The generalization that a <= b <= c gets expanded to a <= b and b <= c is incorrect.Finochio
Z
1

but the real question is how this gets implemented in code.

Do you mean how the interpreter transforms it, or what? You already said

a (logical operator) b (logical operator) c 
    --> (a logical operator b) and (b logical operator c)

so I'm not sure what you're asking here OK, I figured it out: no, you cannot override the expansion from a < b < c into (a < b) and (b < c) IIUC.


I'm curious so that I can replicate this kind of __lt__ and __gt__ behavior in some of my own classes, but I am confused about how this is accomplished holding the middle argument constant.

It depends which of a, b and c in the expression a < b < c are instances of your own class. Implementing your __lt__ and __gt__ and methods gets some of the way, but the documentation points out that:

There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does)

So, if you want Int < MyClass < Int, you're out of luck. You need, at a minimum, MyClass < MyClass < Something (so an instance of your class is on the LHS of each comparison in the expanded expression).

Zoophobia answered 1/10, 2012 at 12:8 Comment(3)
So you are saying that the expansion of a < b < c into a < b and b < c is just hard-coded into the code for this part of the interpreter? There's no way to (aside from branching Python source ), for some classes, expand using numpy.logical_and instead, so that logical arrays are handled correctly?Creepie
Your question starts asking how a chained inequality works, and later says you already know that, and say the real question is something else. So, by about 5 paragraphs in, I'm no longer sure what you're asking, and feel you wasted my time on something you already knew. Now, in the eighth comment to the first answer, and the second comment to the second answer, you've actually made it clear what you wanted to ask. If you feel the answers are dismissive of the question you wanted to ask it's because they're based on the question you actually asked.Zoophobia
... if I have time, I'll try hacking your question into something more answerable. Otherwise, I'd recommend you ask a new question which is clearer.Zoophobia
H
-1

I wanted to respond about the original example--

Why does: (1 < 3) < 2 == True

While: 1 < 3 < 2 == False

So, let's break this out, second (obvious one) first:

(1 < 3) and (3 < 2) simplifies to (True) and (False) which is False

Next, the less obvious one:

(1 < 3) < 2 which simplifies to (True) < 2 which simplifies to 1 < 2, which is True.

Here's another answer that explains that this is because boolean are a subtype of integers: https://mcmap.net/q/76665/-why-are-booleans-considered-integers-duplicate

Here's also the official documentation on boolean being type integer: https://docs.python.org/3/c-api/bool.html?highlight=boolean%20int

Hysteroid answered 6/8, 2023 at 17:10 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Biceps

© 2022 - 2024 — McMap. All rights reserved.