Python Decimal给出了错误的结果

3

我在以下计算中遇到了问题:

Decimal(3)*(Decimal(1)/Decimal(3))

它返回的不是1.0,而是0.999...。即使我增加了Decimal模块的精度也是如此。

以下是完整代码:

from decimal import Decimal
from decimal import getcontext
getcontext().prec = 800
print Decimal(3)*(Decimal(1)/Decimal(3))

具有讽刺意味的是,使用“本地”的float可以解决这个问题:
print float(3)*(float(1)/float(3))

毋庸置疑,我在这里使用Decimal进行更复杂的计算,涉及到大数的指数运算。在遇到这个问题后,我将其最小化为上面的示例。


4
十进制数简单地不能表示分母为3的分数。请查看“fractions”模块,以在有理数上进行精确计算。 - Izaak van Dongen
欢迎来到十进制数和计算机的奇妙世界。有整门数值方法课程教授如何在计算机中进行多次十进制计算时最小化精度损失。 - LhasaDad
1
@LhasaDad:这是工作,不是家庭作业!这里没有“他们”,我正在使用Python实现一些非Pythonic系统的仿真,遇到了这个问题,想在这里寻求建议。 - goodvibration
@AchilleSalaün:是的,这很相关,因为我将其与我模拟的系统的结果进行比较,后者不得大于此结果。然而,在这里,这个条件没有被满足,因为系统的结果(1)实际上比仿真的结果(0.999)要大,这导致我的单元测试报告错误。 - goodvibration
显示剩余4条评论
2个回答

6

对我之前的评论进行详细说明:

在Python中,十进制数字就是你可能手写下来的数字类型。重要的是,它不理解递归的概念,因此像1 / 3这样的无限循环小数只能表示为0.333333333333..,直到达到你设定的精度为止。当这个数乘以3时,你会得到0.99999.. - 这是合理的行为,因为它实际上不能知道那个被截断的0.33333333..1 / 3。由于四舍五入的原因,Decimal经常在除法时失去精度(事实上,当你除以任何除2或5外的因子时都会失去精度)。如果能够进行除法运算非常关键,请使用Fraction,它通过分子和分母表示任何有理数而不会失去任何精度:

In [1]: from fractions import Fraction

In [2]: Fraction(3) * Fraction(1, 3)
Out[2]: Fraction(1, 1)

In [3]: print(_)
1

一个分数将自动简化。
对于您的浮点数,我假设只是幸运的是舍入误差已经抵消了。请注意,除非需要绝对精度,否则float Decimal 可能已经足够好了,在这种情况下,我建议使用Fraction(例如,这意味着您可以可靠地进行等式测试)。您总可以四舍五入一个丑陋的数字,使它看起来至少有点漂亮:
In [4]: "{:.2f}".format(Decimal(3) * (Decimal(1) / Decimal(3)))
Out[4]: '1.00'

如果你正在做模拟之类的事情,0.99999991之间的差异通常并不重要。

另一个选择是重新排列操作顺序,以便分子能够被分母整除,就像Anilkumar的答案所示。如果可能的话,这是一个好的解决方案,但也许有时候你无法这样做——例如,你期望结果是分数,或者你从某种黑盒子中获取了分数乘数。此时,可以跟踪Decimal分子和分母……然后你会意识到这就是Fraction类所做的,但是更简单。

请注意,Fraction可以执行Decimal的所有操作,但更重要的是可以执行更多操作。任何可表示的十进制数也都是一个分数(分子为幂次为10的某个数)。例如:

In [2]: Fraction("3.141")
Out[2]: Fraction(3141, 1000)

自然地,这会导致一些性能损失——分数需要跟踪更多的数据,进行更多的计算,并且可能有点抽象。

看着你新提供的公式——请注意,当你将一个有理数提高到非整数次幂时,结果可能不是有理数,所以你可能会在某个地方失去分数,例如:

>>> Fraction(1, 2) ** 4
Fraction(1, 16)
>>> Fraction(1, 2) ** 0.5
0.7071067811865476

尽管在评估公式的上下文中,试图以符号方式存储所有内容并没有太多意义。这就让我们回到了粗略的 float 足以满足要求的想法上。如果你真的想要某种形式的根式输出格式,可以尝试使用 sympy

In [1]: from sympy import *

In [2]: sqrt(Integer(1) / Integer(2))
Out[2]: sqrt(2)/2

这当然会让你变得更加缓慢。

谢谢,我正在查看您的建议;完成后会更新。 - goodvibration
1
就像我说的,我正在进行一个更复杂的计算,而我问题中的示例只是问题的最小化。实际计算是Decimal(x)*((1+Decimal(y)/Decimal(z))**(Decimal(w)/1000000)-1),目的是模拟在不同(非Pythonic)平台上实现的公式。鉴于这个目的,我可能不能改变公式的顺序(或其中的任何其他内容),所以重新排序的建议对我来说是不可行的。 - goodvibration
谢谢。这个“分数”模块在某些情况下似乎存在一些不准确性。例如,考虑Fraction(9999999999999999999999999999999223)*(1)**(Fraction(10)/Fraction(9)),它返回9999999999999999455752309870428160。当然,如果我认识到这里的指数是1,那么我可以很容易地解决这个特定的情况。然而,我不能开始寻找像这个例子中的特殊情况。有什么想法如何解决这个问题吗? - goodvibration
@goodvibration 那里的问题是 1 ** Fraction(10, 9) 是一个浮点数 1.0(这是因为任何一个数的9/10次方可能不是分数)。整个表达式实际上归结为9999999999999999999999999999999223 * 1.0, 它实际上与 分数 并没有太多关系,而是与 浮点数 的性质有关。现在的问题变成了如何表示无理数,在这种情况下,您应该要么选择近似值,要么选择使用完全的符号表示。当然,在这种情况下 1 ** x 是有理数(1),但我们并不总是知道。 - Izaak van Dongen
任何数提高到9/10次方可能不是分数?你是指“某个数提高到9/10次方可能不是有理数”吗?无论如何,我理解你的观点,但是float归根结底也是有理数,那么为什么这里会有区别呢?无论如何,似乎在所有三个选项(分数、小数和浮点数)中,实际上是Decimal返回了正确的结果。因此,将其替换为Fraction在某些情况下对我有帮助,但在其他情况下会产生问题。有什么建议吗? - goodvibration
显示剩余3条评论

-1

我认为需要更改括号:

from decimal import Decimal
from decimal import getcontext
getcontext().prec = 800
print (Decimal(3)*Decimal(1))/Decimal(3)

对我来说输出是1


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