繁体   English   中英

Python 选择将 integer 除法向负无穷大的数学原因是什么?

[英]What's the mathematical reason behind Python choosing to round integer division toward negative infinity?

我知道 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

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

例如, 为什么在 C++ 中 -1/2 被评估为 0,但在 Python 中被评估为 -1?

抽象地处理这些事情的人倾向于觉得向负无穷大更有意义(这意味着它与数学中定义的模 function 兼容,而不是 % 有一些有趣的含义)。

但我没有看到 C++ 的/与模 function 不兼容。 在 C++ 中, (m/n)*n + m%n == m也适用。

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


我想我应该把这个这是 Guido van Rossum 的旧博客文章放在这个主题上。 在问题正文中,以防人们错过评论。

但是为什么 Python //选择向负无穷大取整?

我不确定最初做出这个选择原因是否在任何地方都有记录(尽管据我所知,它可以在某处的一些 PIP 中详细解释),但我们当然可以想出它为什么会产生的各种原因感觉。

一个原因很简单,向负(或正)无穷大舍入意味着所有数字都以相同的方式四舍五入。 而向零舍入会使零变得特殊。 数学上的说法是,向下舍入到 -∞ 是平移不变的,即它满足方程:

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

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

round_to_zero(0.5 - 1).= round_to_zero(0.5) - 1

当然,其他 arguments 也存在,例如您引用的基于与%运算符(行为)的兼容性(我们希望如何)的参数 - 更多内容如下。

实际上,我想说的真正问题是为什么 Python 的int() function没有定义为将浮点 arguments 舍入为负无穷大,因此m // n将等于int(m / n) (我怀疑“历史原因”。)再说一次,这没什么大不了的,因为 Python 至少有math.floor()满足m // n == math.floor(m / n)


但我没有看到 C++ 的/与模 function 不兼容。 在 C++ 中, (m/n)*n + m%n == m也适用。

没错,但是在将/舍入为零的同时保持该身份需要以一种尴尬的方式为负数定义% 特别是,我们失去了 Python 的以下两个有用的数学属性%

  1. 0 <= m % n < abs(n)对于所有mn
  2. (m + k * n) % n == m % n对于所有整数mnk

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


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

heading = (heading + angle) % 360

这将在所有情况下都有效。

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

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

heading = (heading + angle) % 360;
if (heading < 0) heading += 360;

或作为:

heading = ((heading + angle) % 360) + 360) % 360;

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

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

Python 的 a // b = floor(a/b) 采用标准 (ASCII) 数学符号。 (在德国,高斯符号 [x] 常见于 floor(x)。)地板 function 非常流行(经常使用⇔有用;谷歌查看数百万示例)。 首先可能是因为它简单自然:“最大的 integer ≤ x”。 因此,它具有许多不错的数学特性,例如:

  • 由 integer k 翻译: floor(x + k) = floor(x) + k。
  • 欧几里得除法:x = y · q + r 0 ≤ r < q:= floor(x/y) 对于给定的 x 和 y。

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

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

但是为什么 Python //选择向负无穷大取整?

根据python-history.blogspot.com Guido van Rossum选择了这样的行为//因为

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

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 年开始之前)时间戳和像素方面很有用。

来自 python 文档( https://docs.python.org/2/reference/expressions.ZFC35FDC70D52C79D269EZ883A8 ):

/(除法)和 //(下除法)运算符产生其 arguments 的商。 数字 arguments 首先转换为普通类型。 普通或长 integer 划分产生相同类型的 integer; 结果是数学除法,将“地板”function 应用于结果。

注意在最后一行它说floor function 应用于结果。

So let's see the behaviour of the floor function in python ( https://docs.python.org/3/library/math.html ):

math.floor(x) 返回 x 的下限,最大 integer 小于等于 x。 如果 x 不是浮点数,则委托给 x。 floor (),它应该返回一个 Integral 值。

here the logic of the math.floor(x) function in python is to get the largest integer less than or equal to x and it's the same logic with the floor() function in c++ ( https://www.cplusplus.com/参考/cmath/floor/ )。

But because the division operator in c++ depends on the operands datatypes so it just truncates the fractional part and yields only the integer part of the quotient of the division in case the two operands were integers ( https://www.informit.com/articles /article.aspx?p=352857&seqNum=4 ):

除法转移:您还没有看到有关除法运算符(/)的故事的rest。 此运算符的行为取决于操作数的类型。 如果两个操作数都是整数,则 C++ 执行 integer 除法。 这意味着答案的任何小数部分都将被丢弃,从而使结果为 integer。 如果一个或两个操作数都是浮点值,则保留小数部分,使结果为浮点数。

因此,在两种语言中,地板的逻辑是相同的,并且是向负无穷大舍入,但是对于 python,地板除法实际上调用地板 function,这与 c++ 处理两个整数时的除法行为不同。

为什么floor向负无穷大舍入是因为它是获得小于或等于所应用的数 floor 的最大 integer 的最合理方法。

以数字-6.8为例,对其应用下限 function 结果将是-7现在-7是最大的 integer 小于或等于-6.8因为我们向负无穷大取整,但是如果我们只截断小数部分怎么办的号码?

它将是-6这实际上比我们原来的数字-6.8大,所以我认为这取决于你是要截断小数部分还是要得到除法商的底数。

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

在 C++ 中,它是余数运算符,而在 Python 中,它是模数运算符 - 当两个操作数有不同的符号时,它们不一定是相同的东西。 在以下答案中对这些运算符之间的区别有一些很好的解释: “mod”和“remainder”有什么区别?

现在,考虑到这些差异,integer 除法的舍入(截断)模式必须与两种语言中的一样,以确保在有quotient = dividend / divisormod_or_rem = quotient % divisor之类的表达式时,精确的原始被除数的值由表达式恢复, restored = quotient * divisor + mod_or_rem

这里有两个简短的程序演示了这一点(请原谅我有点天真的 Python 代码——我是该语言的初学者):

C++:

#include <iostream>

int main()
{
    int dividend, divisor, quotient, remainder, check;
    std::cout << "Enter Dividend: ";                        // -27
    std::cin >> dividend;
    std::cout << "Enter Divisor: ";                         // 4
    std::cin >> divisor;

    quotient = dividend / divisor;
    std::cout << "Quotient = " << quotient << std::endl;    // -6
    remainder = dividend % divisor;
    std::cout << "Remainder = " << remainder << std::endl;  // -3

    check = quotient * divisor + remainder;
    std::cout << "Check = " << check << std::endl;          // 27
    return 0;
}

Python:

print("Enter Dividend: ")             # -27
dividend = int(input())
print("Enter Divisor: ")              # 4
divisor = int(input())
quotient = dividend // divisor;
print("Quotient = " + str(quotient))  # -7
modulus = dividend % divisor;
print("Remainder = " + str(modulus))  # 1
check = quotient * divisor + modulus; # -27
print("Check = " + str(check))

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

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

(n+d)/d = (n/d)+1
(-n)/d = -(n/d)

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

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

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

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

Python 不应被视为使用劣等语义,而是决定在重要的情况下通常优越的语义即使在精确的语义无关紧要的情况下也值得稍微减慢除法。

“出于数学原因”

考虑一个问题(在视频游戏中很常见),您的 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

(而且divmod一次给你两个。)

在这里,我为 integer 除法运算符编写div ,为余数运算符编写mod

divmod必须定义为,对于ab非零b整数,我们有

a == (a div b)*b + (a mod b)

通常,您希望mod结果始终介于 0(包括)和b (不包括)之间,而不管a的符号如何。 例如, a可以是循环缓冲区的索引, b可以是其(正)大小。 然后a mod b可以用作底层 memory 数组的索引,即使a是负数。 事实上,使用a == -1来引用最后一个缓冲区元素是非常流行的。

这可能是 Python 将商舍入负无穷大的原因。 因此,余数要么为零,要么具有除数的符号,而与被除数的符号无关。 这是一个基于字母的 plot 用于固定除数:

   ^ y = x mod 5
 4 |   *    *    *    *    *
   |  *    *    *    *    *
   | *    *    *    *    *    *
   |*    *    *    *    *    *
 0 +----*----*----*----*----*->
       -5    0    5   10   15 x

在 C/C++ 中,由于整数的宽度有限,事情变得有点复杂。

假设a == INT_MIN ,在二进制补码表示中是 2 的某个负幂,并且b == 3 如果我们将商四舍五入使得a mod b > 0 ,那么(a div b)*b必须小于INT_MIN ,这将构成有符号整数溢出。 然后效果将由实现定义。 机器甚至会中断执行,例如在使用 GCC 的-ftrapv选项进行编译时。 但是,具体化 integer 除法和余数运算的行为的基本原理是摆脱这种不确定性。

因此,留给 C/C++ 的唯一选择是将商数舍入为零。 因此,余数,如果非零,则具有被除数的符号。

缺点是固定除数的 plot 看起来不太规则:

   ^ y = x mod 5
 4 |             *    *    *
   |            *    *    *
   |           *    *    *    *
   |          *    *    *    *
 0 |    *    *    *    *    *
   |   *    *
   |  *    *
   | *    *
-4 |*    *
   +----+----+----+----+----+->
       -5    0    5   10   15 x

因此, mod buffer-size不会像我们希望的那样处理负索引值。 编程方面,我不喜欢这个决定,尽管即使在极端情况下我也能理解实现a == (a div b)*b + (a mod b)的理由。

Python 选择将 integer 除法舍入为负无穷大的数学原因是它是数学上最一致的选项。 在 Python 中,当您将两个整数相除时,结果将始终是浮点数。 该数字将四舍五入到最接近的 integer,正数向上舍入,负数向下舍入。 这种一致的舍入行为导致向负无穷行为舍入。

Python 将 integer 除法向负无穷大的数学原因是,它比向正无穷大的四舍五入给出更一致的结果。 例如,考虑以下两个表达式:

3 / 4

-3 / 4

第一个表达式将产生值 0.75,而第二个表达式将产生值 -0.75。 这是因为第一个表达式向正无穷大舍入,而第二个表达式向负无穷大舍入。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM