C和Python中模(%)运算的不同行为

77
我发现相同的模运算符在不同编程语言中的结果可能不同。
在Python中:
-1 % 10

产生结果9

在C中它产生-1

  1. 哪个是正确的模?
  2. 如何让C中的模运算与Python相同?
6个回答

90
  1. 两种变体都是正确的,但在数学中(特别是数论方面),Python的模运算最常用。
  2. 在C语言中,你可以使用((n % M) + M) % M来得到与Python相同的结果。例如:((-1 % 10) + 10) % 10。请注意,它仍适用于正整数:((17 % 10) + 10) % 10 == 17 % 10,以及C实现的两种变体(正余数或负余数)。

3
n = -11怎么办?我认为你的意思是 ((n % M) + M) % M。 - Henrik
使用 (-17 + 10) % 10,你仍然会遇到同样的问题。 - yeyeyerman
3
我必须反对第一点,并且要说在数学中两者都是正确的,因为它定义了同余类。 - SurDin
@Checkers不太准确,因为在C语言中它并没有定义真正的同余关系,因为-1%10!= 9%10,而它们显然处于同一个同余类中。 - fortran
如果我理解正确的话,这两个操作仅在一个操作数为负数而另一个操作数不是负数时才有所不同。是这样吗? - Justin Morgan
显示剩余3条评论

39

Python拥有“真实”的取模运算,而C则有一个余数运算。

这与如何处理负整数除法有直接关系,即向0或向负无穷舍入。Python向负无穷舍入,而C(99)向0舍入,但在两种语言中都有 (n/m)*m + n%m == n,因此%运算符必须以正确的方向进行补偿。

Ada更为明确,它同时具有modrem两种方式。


@Svante,将它们都实现为一个库很容易。 - Pacerier
@Pacerier:是的,那是一种绿色编程。取模和余数也是非常低级的操作,因此从高级库中确保效率并不是微不足道的。 - Svante
@Svante,Greenspunning是生活的一部分。即使在Common Lisp中也存在,只不过color不再是绿色了。 - Pacerier
Python的模运算并不是真正的“模”运算,因为它允许M < 0。 - mykhal

17

在C89/90中,负数除法运算符和取余运算符的行为是实现定义的,这意味着根据实现的不同,您可以得到两种不同的行为。只需要确保运算符彼此一致:从a / b = qa % b = r可以推导出a = b * q + r。如果代码对结果有关键依赖,请使用静态断言检查行为。

在C99中,您观察到的行为已成为标准。

实际上,两种行为都有一定的逻辑。Python的行为实现了真正的模操作。您在C中观察到的行为与朝向0舍入一致(这也是Fortran的行为)。

C之所以更喜欢向0舍入,其中一个原因是预期-a / b的结果与-(a / b)相同,这是相当自然的。在真正的模操作行为中,-1 % 10将计算为9,意味着-1 / 10应该是-1。这可能被视为相当不自然,因为-(1 / 10)是0。


2
整数除法是周期性的;(a+kb)/b = (a/b)+k。实数除法是周期性和对称的。试图定义整数除法以添加对称性,除非除数为奇数并且使用四舍五入语义进行计算,否则它将不再是周期性的。我认为周期性比对称性更重要,尽管其他人可能有不同看法。 - supercat

5

两个答案都是正确的,因为-1模109模10相同。

r = (a mod m)
a = n*q + r

您可以确定 |r| < |n|,但无法确定 r 的值。有两个答案,一个是负数,一个是正数。
在 C89 中,尽管答案总是正确的,但模运算(也称为余数)的确切值是未定义的,这意味着它可以是负数或正数。在 C99 中,结果已被定义。
如果您想要正数答案,只需在发现答案为负数时加上 10 即可。
为了使模运算符在所有语言中起作用相同,请记住:
n mod M == (n + M) mod M

通常情况下:

而一般来说:

n mod M == (n + X * M) mod M

7
在C语言中,负数的模运算是有定义的:根据以下语句规定:如果商a/b是可表示的,则表达式(a/b)*b + a%b应等于a - caf
5
我需要澄清一点,在 C 语言中,百分号(%)并不是一个“模运算”符号,而是一个“求余数”的符号。 - caf
1
看起来是这样,我参考了这个维基百科页面,该页面指出在C89中未定义:http://en.wikipedia.org/wiki/Modulo_operation - Brian R. Bondy
1
它的实现是定义的,即它可以是-1或9,但必须是其中之一。 - jk.
@ Brian / jk:答案不正确。C 的当前标准是 C99,而不是 C89,C99 很好地定义了 remainder 运算符,正如 Brian 提供的维基百科网站所清楚说明的那样。 - DevSolar
@DevSolar:当然,C89不是当前的标准。我只是假设(显然是错误的),这个方面没有从C89改变。我重新修改了我的答案。 - Brian R. Bondy

5
执行欧几里得除法 a = b*q + r,就像将分数 a/b 四舍五入为整数商 q,然后计算余数 r 一样。
你看到的不同结果取决于用于四舍五入商的约定...
如果向零舍入(截断),则会获得围绕零对称的结果,例如在 C 语言中:
truncate(7/3) = 2
7 = 3*2 + 1

truncate(-7/3) = -2
-7 = 3* -2 - 1

truncate(7/-3) = -2
7 = -3* -2 + 1

如果你向负无穷取整(floor),你将得到一个类似于Python中的余数:
floor(7/3) = 2
7 = 3*2 + 1

floor(-7/3) = -3
-7 = 3* -3 + 2

floor(7/-3) = -3
7 = -3* -3 - 2

如果你使用四舍五入取整(可以选择四舍五入到偶数或者离零更远的整数),你会得到一个居中的模数:

round(7/3) = 2
7 = 3*2 + 1

round(8/3) = 3
8 = 3*3 - 1

round(-7/3) = -2
-7 = 3* -2 - 1

round(7/-3) = -2
7 = -3* -2 + 1

你可以尝试使用向正无穷取整 (ceil) 的方式实现自己的模运算,这将会创造一种相当不寻常的模运算,但它仍然是模运算的一种形式...

1
自Python 3.7以来,您还可以使用内置模块math中的.remainder()
Python 3.7.0a0 (heads/master:f34c685020, May  8 2017, 15:35:30)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import math
>>> math.remainder(-1, 10)
-1.0

来自文档:

返回x除以y的IEEE 754风格余数。对于有限的x和有限的非零y,这是差值x - n*y,其中n是商x / y的精确值最接近的整数。如果x / y恰好处于两个连续整数之间,则使用最近的偶数整数作为n。因此,余数r = remainder(x, y)总是满足abs(r) <= 0.5 * abs(y)

特殊情况遵循IEEE 754:特别是,对于任何有限x,remainder(x, math.inf)都是x,而remainder(x, 0)remainder(math.inf, x)会针对任何非NaN x引发ValueError异常。如果余数操作的结果为零,则该零将与x具有相同的符号。

在使用IEEE 754二进制浮点数的平台上,此操作的结果始终可以准确表示:不会引入舍入误差。


1
这并没有回答问题的任何一个部分。 - Yola
1
@Yola,你在技术上是正确的,但这个答案为该主题增加了价值。我还会认为,仅仅问“如何使C中的模运算与Python相同?”是不必要的单面的,应该真正包括“如何使Python的行为像C一样”。 - Turun Ambartanen

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