Python中Decimal类型的澄清

45

每个人都知道,或者至少每个程序员都应该知道,使用float类型可能会导致精度错误。但是,在某些情况下,精确的解决方案将是很好的,并且有些情况下,使用epsilon值进行比较是不够的。无论如何,这并不是重点。

我知道Python中的Decimal类型,但从未尝试使用过它。它指出"十进制数可以被准确表示", 我以为这意味着一种聪明的实现方式,可以表示任何实数。我的第一次尝试是:

>>> from decimal import Decimal
>>> d = Decimal(1) / Decimal(3)
>>> d3 = d * Decimal(3)
>>> d3 < Decimal(1)
True

很失望,我回到文档并继续阅读:

算术运算的上下文是指指定精度的环境[...]

好的,所以实际上有一个精度。并且可以重现经典问题:
>>> dd = d * 10**20
>>> dd
Decimal('33333333333333333333.33333333')
>>> for i in range(10000):
...    dd += 1 / Decimal(10**10)
>>> dd
Decimal('33333333333333333333.33333333')

所以,我的问题是:是否有一种具有无限精度的十进制类型?如果没有,比较两个十进制数的更优雅的方法是什么(例如,如果delta小于精度,则d3 < 1应返回False)。
目前,当我只进行除法和乘法时,我使用“分数”类型:
>>> from fractions import Fraction
>>> f = Fraction(1) / Fraction(3)
>>> f
Fraction(1, 3)
>>> f * 3 < 1
False
>>> f * 3 == 1
True

这是最好的方法吗?还有其他选择吗?


11
对于您的假设十进制类型,您希望如何表示 Pi - jfs
1
@J.F.Sebastian 我不会感到惊讶如果这是不可能的。这就是为什么我在问“比较两个十进制数的更优雅的方法是什么”。 - Maxime Chéramy
我还在考虑一种类型,它可以允许许多操作(基本操作、常见函数等),但保持精确表示。例如,分数类型似乎完美地处理了许多情况。实际上,任何时候结果不是无理数都可以使用分数类型。但也许我们可以用另一种表示法做得更好,这种表示法将包括一个更大的子集。例如,像孩子在学校里做的那样具有动态精度的东西。当然,我不能表示“π”,但很多时候,“π”只是问题的一个常数。 - Maxime Chéramy
1
在你重新发明轮子之前,先看看Sage或sympy。 - chthonicdaemon
我正在阅读 SymPy 的文档,但是我找不到一种类型(可以直接替换 Decimal)。但实际上,SymPy 可以进行符号计算,可能可以解决这个问题。 - Maxime Chéramy
5个回答

56

Decimal类最适合用于解决财务类型的加减乘除问题:

>>> (1.1+2.2-3.3)*10000000000000000000
4440.892098500626                            # relevant for government invoices...
>>> import decimal
>>> D=decimal.Decimal
>>> (D('1.1')+D('2.2')-D('3.3'))*10000000000000000000
Decimal('0.0')

分数模块很好地处理了您所描述的有理数问题域。
>>> from fractions import Fraction
>>> f = Fraction(1) / Fraction(3)
>>> f
Fraction(1, 3)
>>> f * 3 < 1
False
>>> f * 3 == 1
True

如果需要科学工作的纯多精度浮点数,请考虑使用mpmath

如果您的问题可以限制在符号领域,请考虑使用sympy。这是如何处理1/3问题的:

>>> sympy.sympify('1/3')*3
1
>>> (sympy.sympify('1/3')*3) == 1
True

Sympy使用mpmath处理任意精度浮点数,具有处理符号有理数和无理数的能力。

考虑到√2的纯浮点表示:

>>> math.sqrt(2)
1.4142135623730951
>>> math.sqrt(2)*math.sqrt(2)
2.0000000000000004
>>> math.sqrt(2)*math.sqrt(2)==2
False

与sympy相比:

>>> sympy.sqrt(2)
sqrt(2)                              # treated symbolically
>>> sympy.sqrt(2)*sympy.sqrt(2)==2
True

您还可以减小数值:

>>> import sympy
>>> sympy.sqrt(8)
2*sqrt(2)                            # √8 == √(4 x 2) == 2*√2...

但是,如果不小心使用Sympy,你可能会遇到类似于普通浮点数的问题:

>>> 1.1+2.2-3.3
4.440892098500626e-16
>>> sympy.sympify('1.1+2.2-3.3')
4.44089209850063e-16                   # :-(

这最好使用 Decimal 完成:

>>> D('1.1')+D('2.2')-D('3.3')
Decimal('0.0')

可以使用分数或 Sympy 并将值,如1.1 保留为比率:

>>> sympy.sympify('11/10+22/10-33/10')==0
True
>>> Fraction('1.1')+Fraction('2.2')-Fraction('3.3')==0
True

或者在sympy中使用有理数(Rational):

>>> frac=sympy.Rational
>>> frac('1.1')+frac('2.2')-frac('3.3')==0
True
>>> frac('1/3')*3
1

你可以使用Sympy Live进行演示。

2
"Chevaux de course" -> "适合不同场地的马" -> 这是英国/澳大利亚的一句谚语,意思是选择适合你要骑行的场地的马匹,即为工作选择合适的人或工具。 - dawg
3
好的 :). 不要对法国人说这个,这对他们来说真的是“什么鬼”。我认为在法语中真的没有相应的表达,也许可以说“(il faut) choisir chaussure à son pied”(需要选择适合自己的鞋子)。 - Maxime Chéramy

6
所以,我的问题是:有没有一种具有无限精度的十进制类型?
不,因为存储无理数需要无限的内存。 Decimal 在表示像货币金额这样的东西时非常有用,其中值需要精确,并且先验知道精度。
从问题中,不完全清楚 Decimal 是否比 float 更适合您的用例。

我对问题的第二部分更感兴趣:“如果不是这样,比较两个十进制数的更优雅的方法是什么(例如,如果精度小于精度,则d3 <1应返回False)。” - Maxime Chéramy
仅仅说“可能需要无限的内存”是不够的,否则就不会有大整数存在。问题在于结果可能是无理数(无法被“分数”精确表示),这样的结果实际上需要无限的内存和/或时间才能计算完毕,例如:sqrt(2)Pi - jfs
1
存储一个无理数需要无限的内存,如果使用浮点数来表示它们,这是必须的。当然,你可以把无理数表示成符号对象。Wolfram alpha、mathematic和sympy都可以实现这一功能。 - dawg
1
@dawg:符号表示仅在您可以通过代数方法获得结果时才有效。否则,我们就不需要数值方法了。 - jfs
更一般地说,实数是不可数的,但可变宽度(有限)位模式是可数的,因此对于任何您可能使用的通用编码,包括涉及任意精度值的编码,总会存在无法在任何给定区间中编码的实数。 - Kevin

3

有没有办法使用无限精度的十进制类型?

不可以;对于实数线上的任何非空区间,你不能使用有限数量的比特表示集合中的所有数字。这就是为什么Fraction很实用,它将分子和分母存储为整数,而整数可以被精确表示:

>>> Fraction("1.25")
Fraction(5, 4)

请注意,Fraction('1.25') 更为安全,因为提供浮点数字面量可能会引入您试图避免的相同问题(即 Fraction 未接收到您打算提供的精确值)。 - chepner
2
“使用有限数量的位无法以无限精度表示整数以外的数字”似乎是误导性的。请考虑可以用符号方式表示诸如π√2等数字并对其进行精确计算的计算机代数系统(例如,√2√3 = √6cos(π)=-1)。这可以在可用内存范围内完成,就像处理大整数一样。我认为你想说的是:对于实线上的任何非空区间,您无法使用有限数量的位以无限精度表示集合中的所有数字。 - svk
@svk感谢您的评论。一种以符号形式表示数字以执行精确计算的类型可能是一个很好的解决方案。是否有Python可用的这样的模块? - Maxime Chéramy

1
如果您是Decimal的新手,这篇文章很有参考价值: Python浮点数任意精度可用吗? 从答案和评论中得出的重要思路是,在需要精确计算的计算机复杂问题中,应该使用mpmath模块https://code.google.com/p/mpmath/。一个重要的观察是,
“在使用Decimal数字时的问题在于无法对Decimal对象执行太多的数学函数操作。”

答案确实很有趣,谢谢。为了补充你的回答,SymPy 允许进行符号数学运算,这可能是处理精确解的好方法。 - Maxime Chéramy
Yup sympy很棒,我在我的研究中使用它(而且它是免费的)。我也使用Mathematica(但它需要花费很多钱)。 - William Denman

1

需要指出的是,可能不是每个人都能立即注意到的一点:

decimal 模块的 文档 表示

…… 精度在算术中得以保留。在十进制浮点数中,0.1 + 0.1 + 0.1 - 0.3 的结果恰好为零。

(也请参见经典文章:浮点数运算是否有问题?

然而,如果我们 简单地 使用 decimal.Decimal,我们将得到相同的“意外”结果。

>>> Decimal(0.1) + Decimal(0.1) + Decimal(0.1) == Decimal(0.3)
False

在上面的简单示例中存在的问题是使用了 float 参数,这些参数会被“无损地转换为其精确的十进制等价物”,如文档所述。
诀窍(在被接受的答案中隐含)是使用字符串构造 Decimal 实例,而不是使用浮点数。
>>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1') == Decimal('0.3')
True

或者,在某些情况下更方便的是使用元组 (<sign>, <digits>, <exponent>)
>>> Decimal((0, (1,), -1)) + Decimal((0, (1,), -1)) + Decimal((0, (1,), -1)) == Decimal((0, (3,), -1))
True

注意:这并不回答原问题,但与之密切相关,并可能对那些基于问题标题到达此处的人有所帮助。

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