负数的除法和取模运算(Python 2.7和C的比较)
一、除法:
除法的取整分为三类:向上取整、向下取整、向零取整。
1.向上取整:向+∞方向取最接近精确值的整数。在这种取整方式下,5 / 3 = 2, -5 / -3 = 2, -5 / 3 = -1, 5 / -3 = -1
2.向下取整:向-∞方向取最接近精确值的整数。在这种取整方式下,5 / 3 = 1, -5 / -3 = 1, -5 / 3 = -2, 5 / -3 = -2
3.向零取整:向0方向取最接近精确值的整数,换言之就是舍去小数部分,因此又称截断取整。在这种取整方式下,5 / 3 = 1, -5 / -3 = 1, -5 / 3 = -1, 5 / -3 = -1通过观察可以发现,无论是向上取整还是向下取整,(-a)/b==-(a/b)都不一定成立。这给程序设计者带来了极大的麻烦。而对于向零取整,(-a)/b==-(a/b)是成立的,以此,C/C++(包括Java)采用这种取整方式。
而Python采用的是向下取整的方式,具体原因得结合取模运算分析。
二、取模:
如果 a/b = q, a%b = r (即a除以b 模q 余r,可表示为a/b=q … r)
那么 a = b*q + r
所以,根据C语言中除法结果向零取整的规则,5 % 3 = 2, -5 % -3 = -2, -5 % 3 = 1, 5 % -3 = –1
即:
a b q r 5 3 1 2 -5 -3 1 -2 -5 3 -1 -2 5 -3 -1 2
那么,为何Python整除运算采用向下取整的规则,详细内容在Why Python's Integer Division Floors?,简单地来讲就是:
因为python认为余数r用到的机会会更大,采用向下取整的规则可以保证余数r与除数b的符号相同(同正或者同负)。以下为重点内容的摘抄:
假设a和b都>=0时,b * q + r = a, 0 <= r < b
如果希望将这一关系扩展到a为负(b仍为正)的情况,有两个选择:一是q向0取整,r取负值,这时约束关系变为 0 <= abs(r) < b,另一种选择是q向下(负无穷方向)取整,约束关系不变,依然是 0 <= r < b。
在数学的数论中,数学家总是倾向于第二种选择(参见如下Wikipedia链接)。
在Python语言中也做了同样选择,因为在某些取模操作应用中被除数a取什么符号并不重要。
例如从POSIX时间戳(从1970年初开始的秒数)得到其对应当天的时间。因为一天有24*3600 = 86400秒,这一操作就是简单的t % 86400。但是当表达1970年之前的时间,这时是一个负数,向0取整规则得到的是一个毫无意义的结果!而向下取整规则得到的结果仍然是正确的。
另外一个我能想到的应用是计算机图形学中计算像素的位置。我相信这样的应用还有更多。顺便说一下,b取负值时,仅需要把符号取反,约束关系变为:
0 >= r > b
那么,现在的问题变成,C为啥不采取(Python)这样的选择呢?可能是设计C时硬件不适合这样做,所谓硬件不适合这样做是说指那些最老式的硬件把负数表示为“符号+大小”而不是像现在的硬件用二进制补码表示(至少对整数是用二进制补码)。我的第一台计算机是一台Control Data大型机,它用1的补码来表示整数和浮点数。60个1的序列表示负0!
Tim Peters对Python的浮点数部分洞若观火,对于我想把这一规则推广到浮点数取模运算有些担心。可能他是对的,因为向负无穷取整的规则有可能导致当x是绝对值特别小的负数时x%1.0会丢失精度。但是这还不足以让我对整数取模,也就是//进行修改。
附言:注意我用了//而不是/,这是一个Python 3 语法,而且在Python 2 中也是有效的,它强调了使用者是要进行整除操作。Python 2 中的 / 有可能产生歧义,因为对两个操作数都是整数时或者一个整数一个浮点数或者两个都是浮点数时,返回的结果类型不同。当然,这是另外的故事,详情参见PEP238。
参考:
[1]http://yangyou230.iteye.com/blog/1315426
[2]http://tieba.baidu.com/p/1881961036
[3]http://blog.chinaunix.net/uid-26898698-id-3269779.html
[4]http://python3.blogspot.kr/2010/08/why-pythons-integer-division-floors.html