Python 选择将整数除法舍入到负无穷大背后的数学原因是什么?

Ric*_*ick 88 c++ python rounding integer-division python-3.x

我知道Python//向负无穷大舍入,而在C++中则/是截断,向0舍入。

到目前为止,这是我所知道的:

               |remainder|
-12 / 10  = -1,   - 2      // C++
-12 // 10 = -2,   + 8      # Python

12 / -10  = -1,     2      // C++
12 // -10 = -2,   - 8      # Python

12 / 10  = 1,      2       // Both
12 // 10 = 1,      2

-12 / -10 = 1,    - 2      // Both
          = 2,    + 8

C++:
1. m%(-n) == m%n
2. -m%n == -(m%n)
3. (m/n)*n + m%n == m

Python:
1. m%(-n) == -8 == -(-m%n)
2. (m//n)*n + m%n == m
Run Code Online (Sandbox Code Playgroud)

但为什么Python//选择向负无穷大舍入呢?我没有找到任何资源解释这一点,但只发现并听到人们含糊地说:“出于数学原因”

例如,为什么在 C++ 中 -1/2 计算为 0,但在 Python 中为 -1?:

抽象地处理这些事情的人们倾向于认为向负无穷大舍入更有意义(这意味着它与数学中定义的模函数兼容,而不是具有有点有趣的含义的 %)。

但我不认为 C++/与模函数不兼容。在 C++ 中,(m/n)*n + m%n == m也适用。

那么 Python 选择向负无穷大舍入的(数学)原因是什么?


另请参阅Guido van Rossum 关于该主题的旧博客文章

Ilm*_*nen 97

\n

但为什么Python//选择向负无穷大舍入呢?

\n
\n

我不确定最初做出这个选择的原因是否任何地方都有记录(尽管据我所知,可以在某个 PEP 中对它进行详细解释),但我们当然可以想出各种原因说得通。

\n

原因之一很简单,向负(或正!)无穷大舍入意味着所有数字都以相同的方式舍入,而向零舍入则使零变得特殊。表达这一点的数学方式是,向下舍入到 \xe2\x88\x92\xe2\x88\x9e 是平移不变式,即它满足以下方程:

\n
\n

round_down(x + k) == round_down(x) + k

\n
\n

对于所有实数x和所有整数k。向零舍入则不会,因为例如:

\n
\n

round_to_zero(0.5 - 1) != round_to_zero(0.5) - 1

\n
\n

%当然,还存在其他参数,例如您基于与(我们希望的)运算符(行为)\xe2\x80\x94 的兼容性而引用的参数,更多内容请参见下面的内容。

\n

事实上,我想说这里真正的问题是为什么 Python 的int()函数没有定义为将浮点参数舍入到负无穷大,因此它m // n等于int(m / n)。(我怀疑“历史原因”。)话又说回来,这没什么大不了的,因为 Python 至少确实math.floor()满足了m // n == math.floor(m / n).

\n
\n
\n

但我不认为 C++/与模函数不兼容。在 C++ 中,(m/n)*n + m%n == m也适用。

\n
\n

确实如此,但是在/向零舍入的同时保留该身份需要%以一种尴尬的方式定义负数。特别是,我们失去了 Python 的以下两个有用的数学属性%

\n
    \n
  1. 0 <= m % n < n对于所有人m和所有积极的n;和
  2. \n
  3. (m + k * n) % n == m % n对于所有整数m,nk.
  4. \n
\n

这些属性很有用,因为 的主要用途之一%是将数字“环绕”m到有限的长度范围n

\n
\n

例如,假设我们正在尝试计算方向:假设我们heading当前的罗盘航向以度为单位(从正北顺时针计数,其中0 <= heading < 360),并且我们想要计算转动angle度数后的新航向(angle > 0如果我们顺时针旋转,或者angle < 0如果我们逆时针旋转)。使用 Python 的%运算符,我们可以简单地计算新的航向:

\n
heading = (heading + angle) % 360\n
Run Code Online (Sandbox Code Playgroud)\n

这在所有情况下都有效。

\n

然而,如果我们尝试在 C++ 中使用这个公式,其不同的舍入规则和相应不同的%运算符,我们会发现环绕并不总是按预期工作!例如,如果我们开始面向西北 ( heading = 315) 并顺时针旋转 90\xc2\xb0 ( angle = 90),我们实际上最终会面向东北 ( heading = 45)。但是,如果然后尝试逆时针转回angle = -9090\xc2\xb0 ( ),则使用 C++ 的%运算符,我们将不会像预期那样返回到 90\xc2\xb0 heading = 315,而是返回到heading = -45!

\n

为了使用 C++ 运算符获得正确的环绕行为%,我们需要将公式编写为:

\n
heading = (heading + angle) % 360;\nif (heading < 0) heading += 360;\n
Run Code Online (Sandbox Code Playgroud)\n

或作为:

\n
heading = ((heading + angle) % 360) + 360) % 360;\n
Run Code Online (Sandbox Code Playgroud)\n

heading = (heading + angle + 360) % 360(只有当我们始终能够保证 时,更简单的公式才有效heading + angle >= -360。)

\n

这是为除法使用非平移不变舍入规则以及因此使用非平移不变%运算符所付出的代价。

\n


Daw*_*weo 16

但为什么Python//选择向负无穷大舍入呢?

根据python-history.blogspot.com ,Guido van Rossum之所以选择这种行为是//因为

(...)有一个很好的数学原因。整数除法运算 (//) 及其兄弟模运算 (%) 一起满足良好的数学关系(所有变量都是整数):

a/b = q 余数 r

这样

b*q + r = a 且 0 <= r < b

(假设 a 和 b >= 0)。

如果您希望关系扩展为负 a(保持 b 为正),您有两种选择:如果将 q 截断为零,r 将变为负数,从而使不变量变为 0 <= abs(r) < 否则,您可以将 q 趋于负无穷大,并且不变量保持 0 <= r < b(...) 在数学数论中,数学家总是更喜欢后一种选择 (...)。对于 Python,我做出了同样的选择,因为有一些有趣的模运算应用,其中 a 的符号并不有趣。考虑采用 POSIX 时间戳(自 1970 年初以来的秒数)并将其转换为一天中的时间。由于一天有 24*3600 = 86400 秒,这样计算就是 t % 86400。但是如果我们用负数来表示 1970 年之前的时间,“向零截断”规则将得到毫无意义的结果!使用底线规则一切都很好。我想到的其他应用是计算机图形学中像素位置的计算。我确信还有更多。

因此,总结一下//行为选择是为了使其与%行为保持一致,选择后者是因为它在处理负数(1970 开始之前)时间戳和像素方面很有用。


Adr*_*ica 12

尽管我无法提供关于为什么/如何选择舍入模式的正式定义,但当您认为这与%您所包含的运算符兼容性的引用确实有意义时,这C++ 和 Python。%

\n

在 C++ 中,它是余数运算符,而在 Python 中,它是运算符 \xe2\x80\x93,并且当两个操作数具有不同符号时,它们不一定是同一件事。在以下问题的答案中对这些运算符之间的差异有一些很好的解释:What's the Difference between \xe2\x80\x9cmod\xe2\x80\x9d and \xe2\x80\x9cremainder\xe2\x80\x9d?

\n

现在,考虑到这种差异,整数除法的舍入(截断)模式必须与两种语言中的相同,以确保您引用的关系(m/n)*n + m%n == m仍然有效。

\n

这里有两个简短的程序在实际中演示了这一点(请原谅我有点不熟练的 Python 代码 \xe2\x80\x93 我是该语言的初学者):

\n

C++:

\n
#include <iostream>\n\nint main()\n{\n    int dividend, divisor, quotient, remainder, check;\n    std::cout << "Enter Dividend: ";                        // -27\n    std::cin >> dividend;\n    std::cout << "Enter Divisor: ";                         // 4\n    std::cin >> divisor;\n\n    quotient = dividend / divisor;\n    std::cout << "Quotient = " << quotient << std::endl;    // -6\n    remainder = dividend % divisor;\n    std::cout << "Remainder = " << remainder << std::endl;  // -3\n\n    check = quotient * divisor + remainder;\n    std::cout << "Check = " << check << std::endl;          // -27\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Python:

\n
print("Enter Dividend: ")             # -27\ndividend = int(input())\nprint("Enter Divisor: ")              # 4\ndivisor = int(input())\nquotient = dividend // divisor;\nprint("Quotient = " + str(quotient))  # -7\nmodulus = dividend % divisor;\nprint("Modulus = " + str(modulus))    # 1\ncheck = quotient * divisor + modulus; # -27\nprint("Check = " + str(check))\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,对于不同符号(-27 和 4)的给定输入,商和余数/模数在语言之间都不同,但恢复的check值在两种情况下都是正确的

\n


sup*_*cat 12

整数和实数算术都定义了它们的除法运算符,以便以下两个等式适用于 n 和 d 的所有值。

(n+d)/d = (n/d)+1
(-n)/d = -(n/d)
Run Code Online (Sandbox Code Playgroud)

不幸的是,整数算术不能以两者都成立的方式定义。出于许多目的,第一个等价比第二个等价更有用,但在代码将两个值相除的大多数情况下,将应用以下其中一个:

  1. 两个值都是正数,在这种情况下,第二个等价项无关紧要。

  2. 被除数是除数的精确整数倍,在这种情况下,两个等式可以同时成立。

从历史上看,处理涉及负数的除法的最简单方法是观察是否恰好有一个操作数为负,去掉符号,执行除法,然后如果恰好有一个操作数为负,则使结果为负。这在两种常见情况下都可以满足要求,并且比使用在所有情况下都保持第一个等价的方法更便宜,而不是仅当除数是股息的精确倍数时。

Python 不应该被视为使用较差的语义,而是认为在重要的情况下通常会更好的语义值得让除法稍微慢一些,即使在精确的语义不重要的情况下也是如此


Kar*_*tel 8

“出于数学原因”

考虑这样一个问题(在视频游戏中很常见):您的 X 坐标可能为负,并且希望将其转换为图块坐标(假设 16x16 图块)以及图块内的偏移量

Python%直接给你偏移量,并/直接给你瓦片:

>>> -35 // 16 # If we move 35 pixels left of the origin...
-3
>>> -35 % 16 # then we are 13 pixels from the left edge of a tile in row -3.
13
Run Code Online (Sandbox Code Playgroud)

(并divmod同时给你两个。)

  • @rick很容易通过一些谷歌搜索来解决这个问题,但实际上它是:假设您已经从起点走了一段距离,并且您想知道您所在的街道街区(“瓷砖坐标”)(假设街道街区彼此之间的距离相等)以及您距该街区起点的距离(“偏移”)。现在想象一下从起点向后走(负距离)。 (2认同)
  • 另一个相关示例:考虑 Unix time_t 值,即自 1970-01-01 以来的秒数。假设您要转换为日期数字以及一天中的秒数。假设您希望它适用于 1970 年 1 月 1 日之前的日期。例如,1969 年 12 月 29 日中午 12:00 将为 -216000。在Python中,-216000/86400会给你第-3天,就像你想要的那样,以及第二天的43200,就像你想要的那样。 (2认同)

Max*_*Max 6

Python 的标准 (ASCII) 数学符号中的 a // b = Floor(a/b)。(在德国,高斯符号 [x] 常见于 Floor(x)。)floor 函数非常流行(经常使用 \xe2\x87\x94 很有用;google 可以看到数百万个例子)。首先可能是因为它简单自然:“最大整数\xe2\x89\xa4 x”。因此,它具有许多很好的数学特性,例如:

\n
    \n
  • 通过整数 k 进行平移:floor(x + k) = Floor(x) + k。
  • \n
  • 欧几里得除法:对于给定的 x 和 y,x = y \xc2\xb7 q + r 且 0 \xe2\x89\xa4 r < q := Floor(x/y)。
  • \n
\n

我能想到的“向零舍入”函数的任何定义都会更加“人为”,并且涉及 if-then 的(可能隐藏在绝对值 |.| 或类似值中)。我不知道有哪本数学书介绍了“向零舍入”函数。这已经是采用这一公约而不是另一公约的充分理由。

\n

我不会在其他答案中详细介绍“与模运算的兼容性”参数,但必须提及它,因为它当然也是一个有效的参数,并且它与上面的“翻译”公式相关联。例如,在三角函数中,当您需要角度模 2 \xcf\x80 时,您肯定需要这种除法。

\n