初学的程序员经常会对浮点运算的不准确感到惊讶。如果他们使用Python,当看到如下的结果时,许多人常常会写帖子说Python "坏了"。
>>> 0.1 + 0.2
这个特殊的结果并不限于Python。事实上,它是如此普遍,以至于存在一个网站,其名称是受这个例子的启发(0.30000000000000004.com/),专门解释这个令人费解的结果的来源,后面还有许多编程语言的例子。
Python 提供了一些标准浮点运算的替代方法。例如,我们可以使用十进制模块来进行定点算术运算。这里有一个例子。
>>> from decimal import Decimal, getcontext
虽然我们可以设置执行运算的精度 (有效数字的数量),但打印出来的数值可能带有额外的零:0.3000000看起来不像0.3那样 "漂亮"。
作为一种选择,Python 提供了分数模块,它支持有理数运算。
>>> from fractions import Fraction
然而,如果不使用字符串参数来表示浮点数,分数模块会产生一些令人惊讶的结果,正如Will McGugan(Rich和Textual的名声)在最近的一条推文中所提到的。
>>> from fractions import Fraction as F
在第二种情况下,0.1是一个浮点数,这意味着它带有一些内在的不精确性。对于第一种情况,在将结果转换为有理数之前,Python会进行一些解析,以确定要使用的小数位数。使用Fraction类的limit_denominator方法也可以得到类似的结果。
>>> F(0.1).limit_denominator(10)
事实上,我们不需要像对分母的限制那样来实现同样的结果。
>>> F(0.1).limit_denominator(1_000_000_000)
虽然我们可以使用Python的特殊模块来实现一些 "更直观 "的浮点运算结果,但人们必须使用的符号并不像 "0.1 + 0.2 "那么简单。正如Raymond Hettinger经常说的:"一定会有更好的方法"。
想法应用
正如本博客的读者已经知道的那样,我创建了一个名为ideas的Python包,以方便创建导入钩子,并使修改后的Python语法易于实验。ideas带有自己的控制台,支持修改的Python语法。它也可以与IPython一起使用(因此也可以与Jupyter笔记本一起使用)。
使用ideas,人们可以 "指示 "Python执行有理算术。 例如,假设我有一个包含以下内容的Python文件。
# simple_math.py
我可以用Python运行它,得到预期的 "不直观 "的结果。
> py simple_math.py
另外,使用思路,我可以用有理算术来执行这个文件。
> ideas simple_math -a rational_math
使用不同的导入钩子,我可以让结果用浮点符号显示。
> ideas simple_math -a nicer_floats
与其执行一个脚本,不如使用想法控制台,从 "nicer_float "开始。
ideas> 0.1 + 0.2
对于 "nicer_float",我也采用了Pyret的符号:紧挨着"~"的浮点数被视为 "近似 "的浮点数,即具有常规的不准确性。
ideas> ~0.1 + 0.2
而且,如前所述,我可以用IPython来使用ideas。这里有一个非常简短的例子
IPython 8.0.0b1 -- An enhanced Interactive Python. Type '?' for help.
最后的想法
考虑到浮点运算对初学者来说是多么令人困惑,我认为如果Python有一个简单的内置方法来切换模式并进行计算,就像上面的例子中用ideas做的那样,那就太好了。然而,我非常怀疑这是否会发生。幸运的是,正如上面所演示的,可以使用导入钩子和修改过的交互式控制台来实现这个结果。