Why is there a difference between `0--3//2` and `--3//2`?

Viewed 4390

I was figuring out how to do floor/ceiling operations without the math module. I solved this by using floor division //, and found out that the negative "gives the ceiling". So this works:

>>> 3//2
1
>>> -3//2
-2

I would like the answer to be positive, so first I tried --3//2, but this gives 1. I inferred this is because Python evaluates -- to +. So to solve this, I found out I could use -(-3//2)), problem solved.

But I came over another solution to this, namely (I included the previous example for comparison):

>>> --3//2  # Does not give ceiling
1
>>> 0--3//2  # Does give ceiling
2

I am unable to explain why including the 0 helps. I have read the documentation on division, but I did not find any help there. I thought it might be because of the evaluation order:

If I use --3//2 as an example, from the documentation I have that Positive, negative, bitwise NOT is strictest in this example, and I guess this evaluates -- to +. Next comes Multiplication, division, remainder, so I guess this is +3//2 which evaluates to 1, and we are finished. I am unable to infer it from the documentation why including 0 should change the result.

References:

3 Answers

Python uses the symbol - as both a unary (-x) and a binary (x-y) operator. These have different operator precedence.

In specific, the ordering wrt // is:

  • unary -
  • binary //
  • binary -

By introducing a 0 as 0--3//2, the first - is a binary - and is applied last. Without a leading 0 as --3//2, both - are unary and applied together.

The corresponding evaluation/syntax tree is roughly like this, evaluating nodes at the bottom first to use them in the parent node:

 ---------------- ---------------- 
|     --3//2     |    0--3//2     |
|================|================|
|                |    -------     |
|                |   | 0 - z |    |
|                |    -----+-     |
|                |         |      |
|     --------   |     ----+---   |
|    | x // y |  |    | x // y |  |
|     -+----+-   |     -+----+-   |
|      |    |    |      |    |    |
|  ----+    +--  |   ---+    +--  |
| | --3 |  | 2 | |  | -3 |  | 2 | |
|  -----    ---  |   ----    ---  |
 ---------------- ---------------- 

Because the unary - are applied together, they cancel out. In contrast, the unary and binary - are applied before and after the division, respectively.

This is a simple matter of order of operations.

--3//2 is the same as (-(-3)) // 2. Since there is nothing on the left-hand side, each - must be unary negation; and this has higher precedence than //; so 3 is negated twice (yielding 3) and then divided by 2.

0--3//2 is the same as 0 - ((-3) // 2). Now that there is something on the left-hand side, the first - must be binary subtraction, which has lower precedence than //. The second - is still unary negation; -3 is divided by 2 yielding -2, and then that value is subtracted from 0.

Another way to know how CPython actually calculates is to use the dis module to see what it actually does with its stack machine.

>>> import dis
>>> dis.dis('0--3//2')
  1           0 LOAD_CONST               0 (2)
              2 RETURN_VALUE

Whoops, constants are calculated during compilation, so use a name.

>>> t=3
>>> dis.dis('0--t//2')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_NAME                0 (t)
              4 UNARY_NEGATIVE
              6 LOAD_CONST               1 (2)
              8 BINARY_FLOOR_DIVIDE
             10 BINARY_SUBTRACT
             12 RETURN_VALUE
Related