为什么 `0--3//2` 和 `--3//2` 之间有差异?

63

我正在想如何在没有math模块的情况下执行地板和天花板操作。我通过使用地板除法 // 来解决了这个问题,并发现负数“给出天花板”。所以这样做可以实现:

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

我希望答案是正数,所以我先尝试了--3//2,但得到的结果是1。我推断这是因为Python将--评估为+。所以为了解决问题,我发现我可以使用-(-3//2)),问题解决了。

但是我找到了另一种解决方案,即(为了比较,我包含了之前的示例):

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

我无法解释为什么包括0会起到帮助作用。我已经阅读了除法文档,但是没有发现任何有用的信息。我认为这可能是由于表达式求值顺序的原因:

如果以--3 // 2为例,从文档中可以得知,在这个例子中,运算符的严格性排列顺序是:正数、负数、按位取反,我猜测它将--评估为+。其次是乘法、除法、取模,所以我猜测这是+3 // 2,它计算为1,我们完成了。我无法从文档中推断出为什么包括0会改变结果。

参考资料:


6
-3//2 得到 -2 仍然得到 floor,其中 floor(x) 表示小于 x 的最大整数,-2 是小于 -1.5 的最大整数。 - Hearth
2
@chepner 我认为他们的意思是想用这个操作作为正数向上取整的一种绕路方式。因此,他们不希望-3//2是正数,而是希望以一种可以有效地使用math.ceil(3/2)的方式将结果一致地转换为正数。 - Tyberius
2
@chepner “-3//2”的目的只是为了让他们得到“3/2”的“ceil”。第二步是将其转换回正/绝对值,这触发了这个问题,因为它并没有完全按照预期进行。 - MisterMiyagi
1
顺便说一句,我经常使用“-(-a // b)”结构,但通常会加注释,因为它不是常见的习惯用法。顺便提一下,它比“0 - -a // b”略微更有效率。当然,任何一个版本都比使用“math.ceil”函数调用更有效率。此外,“math.ceil”对于非常大的整数也没有用处。 - PM 2Ring
2
@ttbek:它比向下取整加一的方法更优,因为它可以得到正确的答案!(例如,如果a=4,b=2,则(a // b) +1会得到3,但是ceil(4/2)是2。) - psmears
显示剩余7条评论
3个回答

86

Python使用符号-既作为一元运算符(-x)又作为二元运算符(x-y)。这两个操作符具有不同的运算优先级

具体而言,对于//的顺序如下:

  • 一元-
  • 二元//
  • 二元-

通过在0--3//2中引入0,第一个-二元-,最后应用。如果没有前导0,则--3//2中的两个-都是一元的并一起应用。

相应的求值/语法树大致如下,首先评估底部的节点然后在父节点中使用它们:

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

由于一元符号-是同时应用的,它们会相互抵消。相反,一元和二元符号-分别在除法 之前之后 应用。


8
啊,我是个新手,所以“一元”和“二元”的区别让我有些困惑。在你的回答之后,我尝试了这个表达式:0+--3//2 == 1,所以第一个操作符在0右边被视为“二元”操作符是有道理的。 - Karl Wilhelm
21
在你的代码中添加空格和括号将使其更清晰易懂。除非你是在为 CodeGolf.SE 编写代码并尝试节省字节,否则最好更清晰地格式化代码。 - Darrel Hoffman
5
@KarlWilhelm 一元操作是指只接受一个参数的函数。二元操作则是指接受两个参数的函数。在解释发生了什么时,画出括号会很有帮助。例如 0--3//20 - (-3 // 2),而 --3//2 则是 (--3) // 2 - Cort Ammon
2
现在我想知道在Python中,“-3”中的“-”是一元运算符还是整数字面量的一部分。 - Carsten S
7
@CarstenS 从语法上来说,它不是字面上的一部分 - 抽象语法树基本上将其表示为UnaryOp("-", 3)(只是不那么漂亮)。 整数字面值仅仅是绝对值。但是,在生成字节码时被评估,并在运行时直接作为值“-3”加载。 - MisterMiyagi
显示剩余2条评论

30

这是一个简单的运算顺序问题。

--3//2等同于(-(-3)) // 2。由于左边没有任何内容,每个 - 必须是一元负号; 这比 // 的优先级更高,因此 3 被取反两次(得到3),然后再除以 2。

0--3//2等同于0 - ((-3) // 2)。现在左侧有内容,则第一个 - 必须是二元减法,其优先级比 // 更低。第二个 - 仍然是一元负号;-3 除以 2 得到 -2,然后该值从 0 中减去。


1
修正了拼写错误。毕竟里面有很多小数字。 - Karl Knechtel

18

了解CPython实际计算的另一种方法是使用dis模块,查看其栈机器实际执行的操作。

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

糟糕,常量是在编译时计算的,因此请使用名称。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接