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。