Python中,数,用各种形式表示,不同形式的数有各自的用途。
整数
整数,令人惊叹于它的简单。两个整数相除,例如4/3
,得到一个浮点数,并且(4/3)*3
的结果也是浮点数4.0
。即便你没有定义浮点数,在进行除法运算的时候,它会自动出现。
浮点数
浮点数不是一般意义的数。按照数学上的规定,数应该遵循如下原则:减法是加法的逆运算,加法结合律,等等。
例如:
>>> 1 + 2 - 2 - 1
0
>>> 0.1 + 0.2 - 0.2 - 0.1
2.7755575615628914e-17
两个数相加,再分别减去它们,上述居然出现了不同的结果。
它们也不会遵循结合律:a + (b + c) = (a + b) + c
>>> a = 2**-53
>>> (a + a) + 1 == a + (a + 1)
False
以上仅仅是浮点数运算中存在的两个“小问题”,还不令你惊讶吗?此处不便将浮点数各种出乎意料的运算一一展现。
分数
很多看似简单的程序,遇到分数,就会出问题,比如运算时间暴增,算法的复杂度加倍。遇到分数的时候,算法时间不是跟输入成正比,而是指数增长。
如果时间足够长,内存爆掉也是常见的。
加法就是其中一个典型例子
>>> print(set(type(p) for p in primes))
>>> one = fractions.Fraction(1)
>>> before = datetime.now()
>>> res = sum(one/p for p in primes[:10000])
>>> after = datetime.now()
>>> print("It took", after-before)
>>> print("Size of output", len(str(res)))
>>> print("Approximate value", float(res))
{<class 'int'>}
It took 0:01:16.033260
Size of output 90676
Approximate value 2.7092582487972945
这段程序,计算了一些素数的倒数的和。在笔记本电脑上,10000个这样的数相加,要1分钟,最终输出结果的大小超过了90K。
对比着,执行浮点数运算,性能更好。
>>> print(set(type(p) for p in primes))
>>> before = datetime.now()
>>> res = sum(1/p for p in primes[:10000])
>>> after = datetime.now()
>>> print("It took", after-before)
>>> print("Size of output", len(str(res)))
>>> print("Approximate value", float(res))
{<class 'int'>}
It took 0:00:00.000480
Size of output 17
Approximate value 2.709258248797317
这次运行时间小于1毫秒,并且,者还可能是因为用datetime
测量产生的误差,快了10000倍。而且输出结果的大小仅有17比特,下降了1000多倍。然而,计算结果有误差。
Approximate value 2.7092582487972945
Approximate value 2.709258248797317
1234567891234
误差低于 10的-14次方,这就如同将火箭发射月球上偏差了1毫米,用浮点数计算得到的结果足够精确,并且效率更高。
对此,一般的观点是:Python进行分数运算很慢。对此,Python可以承担10倍的责任,但不是10000倍。有一个第三方模块,quicktions,用Cython执行分数的运算。
用quicktions,真的“很快”。在我的笔记本电脑上,上面那个程序的时间,从1分16秒,缩短到1分15秒。
问题在于程序本身,在程序中,我精心选择了一种输入方案,以素数作为分母进行分数相加,这本来就是一种很坏的情况。
小数
小数在财务中用途最广,最无聊的是居然以法律的方式规定了小数的形式。然而,Python中所有的小数点运算,都有上下文精确度问题,对此,可以用专门的模块解决。
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
在实际项目中,代码中设置精度的位置和进行计算的位置可能间隔几百行,计算可以在一个函数中,也可以在另外一个文件。
最安全的方法是使用localcontext
:
>>> getcontext().prec = 6
>>> # 6853 lines elided
... with localcontext() as ctx:
... ctx.prec = 10
... Decimal(1) / Decimal(7)
...
Decimal('0.1428571429')
只要你认真地用localcontext
,小数运算不会出问题。
总结
你在程序中用到数字的时候,是否想过:应该用什么类型?会发生什么?误差重要吗?
什么也不想,会意味着暗藏bug。