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.

ely
  • 74,674
  • 34
  • 147
  • 228

4 Answers4

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

FogleBird
  • 74,300
  • 25
  • 125
  • 131
  • 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. – ely Oct 01 '12 at 14:26
  • 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. – ely Oct 01 '12 at 14:42
  • 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__`. – ely Oct 01 '12 at 14:44
  • @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. – seberg Oct 04 '12 at 00:24
  • 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`. – ely Oct 04 '12 at 03:07
  • @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. – seberg Oct 04 '12 at 08:41
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.

stochastic
  • 3,155
  • 5
  • 27
  • 42
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • Why doesn't it result in (1 <= 3) --> True --> (True >= 2) --> type error? – ely Sep 30 '12 at 02:46
  • 1
    The expression `a <= b <= c` gets expanded to `a <= b and b <= c` – Óscar López Sep 30 '12 at 02:50
  • 1
    In code, how does the expansion happen. Or is this just a one-off hard-coded Python idiom? – ely Sep 30 '12 at 02:52
  • See my expanded question above. – ely Sep 30 '12 at 03:00
  • 1
    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. – Useless Oct 01 '12 at 12:13
  • @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. – DSM Oct 01 '12 at 12:15
  • 1
    The generalization that `a <= b <= c` gets expanded to `a <= b and b <= c` is incorrect. – FogleBird Oct 01 '12 at 14:40
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).

Useless
  • 64,155
  • 6
  • 88
  • 132
  • 1
    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? – ely Oct 01 '12 at 12:35
  • 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. – Useless Oct 01 '12 at 13:43
  • ... 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. – Useless Oct 01 '12 at 13:44
-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://stackoverflow.com/a/47644907/22348177

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

  • 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](/help/how-to-answer). – Community Aug 09 '23 at 17:35