【问题标题】:What's the mathematical reason behind Python choosing to round integer division toward negative infinity?Python 选择将整数除法舍入到负无穷大的数学原因是什么?
【发布时间】:2022-01-16 14:07:32
【问题描述】:

我知道 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 // 选择向负无穷取整呢?我没有找到任何资源来解释这一点,但只是找到并听到人们含糊地说:“出于数学原因”

例如,在Why is -1/2 evaluated to 0 in C++, but -1 in Python?

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

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

那么 Python 选择向负无穷取整的(数学)原因是什么?


另见Guido van Rossum's old blog post on the topic

【问题讨论】:

  • 请注意,% 运算符在 C++ 和 Python 中相当相同:在 C++ 中,它是一个 remainder 运算符,但在Python,它是一个 modulus 运算符。 Nice answer explaining the difference.
  • 虽然(m/n)*n + m%n == m 规则适用,但m%n 的可能输出是[-n+1,n-1],它是[0,n-1] 的两倍。而且对于多种用途来说非常不方便。相反,他们选择了符号不变性......它也有它的好处。一般而言,问题在于人们希望从四舍五入中获得多个属性,但并非所有属性都可以同时实现 - 所以他们会选择自己喜欢的。
  • @user438383:我将您的编辑修改为标题,该标题表示将 to 舍入为负无穷而不是 toward。这将是一个关于某些输入产生实际 -Inf 作为某种结果的问题,但这里不是这种情况。

标签: python c++ python-3.x rounding integer-division


【解决方案1】:

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

我不确定最初做出此选择的原因是否在任何地方都有记录(尽管据我所知,它可以在某处的某些 PEP 中详细解释),但我们当然可以想出各种理由说明它是有意义的。

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

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

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

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

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

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


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

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

  1. 0 <= m % n < n 代表所有 m 和所有正面 n;和
  2. (m + k * n) % n == m % n 用于所有整数 mnk

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


例如,假设我们正在尝试计算方向:假设heading 是我们当前的compass 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 &gt;= -360 时才有效。)

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

【讨论】:

  • 多么生动的例子!我详细阅读了你的答案,我同意你的看法。和我猜的差不多,对于a/b = q, r,Python之所以采用这种取整方式是为了让a%b有实际意义/可以匹配现实生活中的例子,当a &lt; 0b &gt; 0时。见Here's Guido van Rossum's old blog post on the topic.,由@user2357112提供,在问题评论中支持莫妮卡。
  • 虽然对于a/b=q, r,当a &gt; 0b &lt; 0 得到r &lt; 0 时,我想不出任何与它相匹配的现实生活中有意义的例子。在尝试制作a%b= r &gt; 0a &lt; 0b &gt; 0 时,我会将其视为沿途发生的事情。
  • 我也认为:“1. 0 &lt;= m % n &lt; abs(n) 代表所有 mn;”不是 100% 正确的。一个反例是12 % -10 = -8。我认为应该是“0 &lt;= m % n &lt; n for all m, and n &gt; 0”。
  • Python 至少有 math.floor() 满足 m // n == math.floor(m / n)。它适用于小数字,但对于较大的数字,它并不总是因为 m / n 不会产生精确的结果,它会产生一个四舍五入到浮点数的结果。
  • (未优化)C代码主要是CPU指令的一对一表示,所以我猜(我没有任何来源)决定模运算符如何工作的不是C设计者,但是处理器设计者。所以我猜(同样没有来源)以这种方式实现模运算可能更快或需要更少的逻辑门,不一定要“定义一种尴尬的方式”
【解决方案2】:

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

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

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

a/b = q 余数为 r

这样

b*q + r = a 和 0

(假设 a 和 b >= 0)。

如果您希望关系扩展为负 a(保留 b 正),你有两个选择:如果你将 q 截断为零,r 将变为负数,因此不变量变为 0

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

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

    【讨论】:

      【解决方案4】:

      虽然我不能提供一个正式的定义为什么/如何选择舍入模式,但关于与% 运算符兼容性的引用,您已包含,确实当您认为% 在 C++ 和 Python 中完全不是一回事时才有意义。

      在 C++ 中,它是 remainder 运算符,而在 Python 中,它是 modulus 运算符 - 并且,当两个操作数具有 不同 em> 符号,这些不一定是同一件事。在What's the difference between “mod” and “remainder”?的答案中对这些运算符之间的区别进行了一些很好的解释

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

      这里有两个简短的程序演示了这一点(请原谅我有点幼稚的 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("Modulus = " + str(modulus))    # 1
      check = quotient * divisor + modulus; # -27
      print("Check = " + str(check))
      

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

      【讨论】:

      • “当两个操作数有不同的符号时,这些不一定是同一件事”--> 我认为是“当第一个操作数为负时,这些不一定是同一件事”@ 987654322@。也许在这里试试27 and -4
      • @chux 对于 27 和 -4,C++ 代码为余数提供 3,而 Python 代码为模数提供 -1。请参阅this answer 的顶部。
      • ...当然,当余数为零时,模数也为零(反之亦然)-因此我说“不一定...”跨度>
      • 啊,我明白了,Python mod 不是Euclidean mod
      【解决方案5】:

      “出于数学原因”

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

      【讨论】:

      • 平移不变性也是@ilmari说的。
      • 是的,这就是基本的数学概念;我正在展示另一个实际示例,说明您为什么会关心它。 (另一张海报给出了指南针方向的例子,这也与许多电子游戏相关。)
      • 什么是平铺坐标? ? 我想但无法理解这个例子。缺乏一些背景知识。
      • @rick 很容易通过一些谷歌搜索来解决这个问题,但实际上它是:假设您已经从起点走了一段距离,并且您想知道哪个街区(“瓷砖坐标”)您所在的位置(假设街区之间的距离都相等)以及您距该街区的起点有多远(“偏移”)。现在想象一下从起点向后走(负距离)。
      • 另一个相关示例:考虑 Unix time_t 值,它是自 1970-01-01 以来的秒数。假设您要转换为天数和一天内的秒数。假设您希望这适用于 1970-01-01 之前的日期。因此,例如,1969 年 12 月 29 日中午 12:00 将是 -216000。在 python 中,-216000/86400 会给你第 -3 天,就像你想要的那样,给你一天之内的第 43200 天。
      【解决方案6】:

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

      • 整数 k 的转换:floor(x + k) = floor(x) + k。
      • 欧几里得除法:x = y · q + r 其中 0 ≤ r

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

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

      【讨论】:

        【解决方案7】:

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

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

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

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

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

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

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

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

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

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

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

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

        【讨论】:

          【解决方案8】:

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

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

          3 / 4

          -3 / 4

          第一个表达式的结果是 0.75,而第二个表达式的结果是 -0.75。这是因为第一个表达式向正无穷大舍入,而第二个表达式向负无穷大舍入。

          【讨论】:

          猜你喜欢
          • 2017-01-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-11
          相关资源
          最近更新 更多