143

Looking into Queue.py in Python 2.6, I found this construct that I found a bit strange:

def full(self):
    """Return True if the queue is full, False otherwise
    (not reliable!)."""
    self.mutex.acquire()
    n = 0 < self.maxsize == self._qsize()
    self.mutex.release()
    return n

If maxsize is 0 the queue is never full.

My question is how does it work for this case? How 0 < 0 == 0 is considered False?

>>> 0 < 0 == 0
False
>>> (0) < (0 == 0)
True
>>> (0 < 0) == 0
True
>>> 0 < (0 == 0)
True
user3840170
  • 26,597
  • 4
  • 30
  • 62
Marcelo Santos
  • 1,851
  • 1
  • 12
  • 14
  • 3
    @Marino Šimić: From the second example shown in the OP's question, `>>> (0) < (0 == 0)`, it clearly isn't. – martineau May 20 '11 at 17:13
  • 3
    One reason you shouldn't be writing code like `n = 0 < self.maxsize == self._qsize()` in the first place, in any language. If your eyes have to dart back and forth across the line several times to figure out what's going on, it's not a well-written line. Just split it up into several lines. – BlueRaja - Danny Pflughoeft May 20 '11 at 21:29
  • 2
    @Blue: I agree with not writing such a comparison that way but splitting it into separate lines is going a bit overboard for two comparisons. I hope you mean, split it up into separate comparisons. ;) – Jeff Mercado May 21 '11 at 06:37

9 Answers9

118

Python has special case handling for sequences of relational operators to make range comparisons easy to express. It's much nicer to be able to say 0 < x <= 5 than to say (0 < x) and (x <= 5).

These are called chained comparisons.

With the other cases you talk about, the parentheses force one relational operator to be applied before the other, and so they are no longer chained comparisons. And since True and False have values as integers you get the answers you do out of the parenthesized versions.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Omnifarious
  • 54,333
  • 19
  • 131
  • 194
  • it's interesting to try some of these comparisons and specify int() and bool(). I realized that the bool() of any non-zero is 1. Guess I never would have even tried to directly specify anything other than bool(0) or bool(1) before this thought experiment – j_syk May 20 '11 at 15:45
43

Because

(0 < 0) and (0 == 0)

is False. You can chain together comparison operators and they are automatically expanded out into the pairwise comparisons.


EDIT -- clarification about True and False in Python

In Python True and False are just instances of bool, which is a subclass of int. In other words, True really is just 1.

The point of this is that you can use the result of a boolean comparison exactly like an integer. This leads to confusing things like

>>> (1==1)+(1==1)
2
>>> (2<1)<1
True

But these will only happen if you parenthesise the comparisons so that they are evaluated first. Otherwise Python will expand out the comparison operators.

Katriel
  • 120,462
  • 19
  • 136
  • 170
  • 2
    I saw an interesting use for boolean values being used as integers yesterday. The expression `'success' if result_code == 0 else 'failure'` can be rewritten as `('error', 'success')[result_code == 0]`, before this I had never seen a boolean used to select an item in a list/tuple. – Andrew Clark May 20 '11 at 16:24
  • 'bool' was added sometime around Python 2.2. – MRAB May 20 '11 at 19:59
18

The strange behavior your experiencing comes from pythons ability to chain conditions. Since it finds 0 is not less than 0, it decides the entire expression evaluates to false. As soon as you break this apart into seperate conditions, you're changing the functionality. It initially is essentially testing that a < b && b == c for your original statement of a < b == c.

Another example:

>>> 1 < 5 < 3
False

>>> (1 < 5) < 3
True
Tyler
  • 1,377
  • 1
  • 11
  • 18
9
>>> 0 < 0 == 0
False

This is a chained comparison. It returns true if each pairwise comparison in turn is true. It is the equivalent to (0 < 0) and (0 == 0)

>>> (0) < (0 == 0)
True

This is equivalent to 0 < True which evaluates to True.

>>> (0 < 0) == 0
True

This is equivalent to False == 0 which evaluates to True.

>>> 0 < (0 == 0)
True

Equivalent to 0 < True which, as above, evaluates to True.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
8

Looking at the disassembly (the bytes codes) it is obvious why 0 < 0 == 0 is False.

Here is an analysis of this expression:

>>>import dis

>>>def f():
...    0 < 0 == 0

>>>dis.dis(f)
  2      0 LOAD_CONST               1 (0)
         3 LOAD_CONST               1 (0)
         6 DUP_TOP
         7 ROT_THREE
         8 COMPARE_OP               0 (<)
        11 JUMP_IF_FALSE_OR_POP    23
        14 LOAD_CONST               1 (0)
        17 COMPARE_OP               2 (==)
        20 JUMP_FORWARD             2 (to 25)
   >>   23 ROT_TWO
        24 POP_TOP
   >>   25 POP_TOP
        26 LOAD_CONST               0 (None)
        29 RETURN_VALUE

Notice lines 0-8: These lines check if 0 < 0 which obviously returns False onto the python stack.

Now notice line 11: JUMP_IF_FALSE_OR_POP 23 This means that if 0 < 0 returns False perform a jump to line 23.

Now, 0 < 0 is False, so the jump is taken, which leaves the stack with a False which is the return value for the whole expression 0 < 0 == 0, even though the == 0 part isn't even checked.

So, to conclude, the answer is like said in other answers to this question. 0 < 0 == 0 has a special meaning. The compiler evaluates this to two terms: 0 < 0 and 0 == 0. As with any complex boolean expressions with and between them, if the first fails then the second one isn't even checked.

Hopes this enlightens things up a bit, and I really hope that the method I used to analyse this unexpected behavior will encourage others to try the same in the future.

SatA
  • 537
  • 1
  • 6
  • 15
  • Wouldn't it just be easier to work it out from the spec rather than reverse engineering one particular implementation? – David Heffernan Jul 04 '14 at 13:43
  • No. That's the short answer. I believe that it depends on your personality. If you relate to a "black box" view and prefer getting your answers from specs and documentation then this answer will only confuse you. If you like to dig in and reveal the internals of stuff, then this answer is for you. Your remark that this reverse engineering is relevant only to one particular implementation is correct and must be pointed out, but that was not the point of this answer. It is a demonstration on how easy it is in python to take a glance "under the hood" for those who are curious enough. – SatA Jul 08 '14 at 06:56
  • 1
    The problem with reverse engineering is its lack of predictive power. It's not the way to learn a new language. – David Heffernan Jul 08 '14 at 07:04
  • Another thing that I must add, is that specs and documentations aren't always complete and in most cases won't provide answers for such specific cases. Then, I believe, one mustn't be afraid to explore, investigate and reach deeper as much needed to get the answers. – SatA Jul 08 '14 at 07:06
  • Byte code is not an explanation for why operators chain. Operator chaining is the explanation for the byte code and is [well documented](https://docs.python.org/3/reference/expressions.html#comparisons). – chepner Mar 18 '22 at 16:36
  • Right, the byte code isn't the explanation for "why". That can be found in the documentation as stated several times in this thread. However, byte code can be used to understand how the expression is interpreted internally. – SatA Mar 19 '22 at 19:29
2

maybe this excerpt from the docs can help:

These are the so-called “rich comparison” methods, and are called for comparison operators in preference to __cmp__() below. The correspondence between operator symbols and method names is as follows: x<y calls x.__lt__(y), x<=y calls x.__le__(y), x==y calls x.__eq__(y), x!=y and x<>y call x.__ne__(y), x>y calls x.__gt__(y), and x>=y calls x.__ge__(y).

A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments. 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.

There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected. See the paragraph on __hash__() for some important notes on creating hashable objects which support custom comparison operations and are usable as dictionary keys.

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); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

Arguments to rich comparison methods are never coerced.

These were comparisons but since you are chaining comparisons you should know that:

Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

Formally, if a, b, c, ..., y, z are expressions and op1, op2, ..., opN are comparison operators, then a op1 b op2 c ... y opN z is equivalent to a op1 b and b op2 c and ... y opN z, except that each expression is evaluated at most once.

SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
Marino Šimić
  • 7,318
  • 1
  • 31
  • 61
2

As other's mentioned x comparison_operator y comparison_operator z is syntactical sugar for (x comparison_operator y) and (y comparison_operator z) with the bonus that y is only evaluated once.

So your expression 0 < 0 == 0 is really (0 < 0) and (0 == 0), which evaluates to False and True which is just False.

dr jimbob
  • 17,259
  • 7
  • 59
  • 81
1

Here it is, in all its glory.

>>> class showme(object):
...   def __init__(self, name, value):
...     self.name, self.value = name, value
...   def __repr__(self):
...     return "<showme %s:%s>" % (self.name, self.value)
...   def __cmp__(self, other):
...     print "cmp(%r, %r)" % (self, other)
...     if type(other) == showme:
...       return cmp(self.value, other.value)
...     else:
...       return cmp(self.value, other)
... 
>>> showme(1,0) < showme(2,0) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
False
>>> (showme(1,0) < showme(2,0)) == showme(3,0)
cmp(<showme 1:0>, <showme 2:0>)
cmp(<showme 3:0>, False)
True
>>> showme(1,0) < (showme(2,0) == showme(3,0))
cmp(<showme 2:0>, <showme 3:0>)
cmp(<showme 1:0>, True)
True
>>> 
Ry-
  • 218,210
  • 55
  • 464
  • 476
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304
0

I'm thinking Python is doing it's weird between magic. Same as 1 < 2 < 3 means 2 is between 1 and 3.

In this case, I think it's doing [middle 0] is greater than [left 0] and equal to [right 0]. Middle 0 is not greater than left 0, so it evaluates to false.

mpen
  • 272,448
  • 266
  • 850
  • 1,236