Why is there a difference between `0–3//2` and `–3//2`?
Question:
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:
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
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:
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