Python浮点数转为Decimal的转换方法

89

Python Decimal不支持从float构建,它期望你首先将float转换为字符串。

这非常不方便,因为浮点数的标准字符串格式化需要指定小数位数而不是有效位数。因此,如果你有一个可能有多达15个小数位的数字,你需要格式化为Decimal("%.15f" % my_float),如果在小数点前也有任何有效位数,则第15个小数位将得到错误结果(Decimal("%.15f" % 100000.3) == Decimal('100000.300000000002910'))。

有人能否建议一种好的方法来将float转换为Decimal并保留值,或许限制可以支持的有效位数?


5
请注意,在Python 2.7中,您无需先转换为字符串,Decimal(0.1)现在是有效的,它打印为Decimal('0.1000000000000000055511151231257827021181583404541015625')。 - Nick Craig-Wood
13个回答

79

Python <2.7

翻译为:

Python小于2.7版本

"%.15g" % f

或者在Python 3.0中:

format(f, ".15g")

Python 2.7+, 3.2+

只需直接将浮点数传递给Decimal构造函数即可,如下所示:

from decimal import Decimal
Decimal(f)

36
将浮点数直接传递给Decimal构造函数会引入舍入误差。最好在将浮点数传递给构造函数之前将其转换为字符串。例如,Decimal(0.30000000000000004)的结果为Decimal('0.3000000000000000444089209850062616169452667236328125'),而Decimal(str(0.30000000000000004))的结果为Decimal('0.30000000000000004') - Aliaksandr Adzinets
2
@AliaksandrAdzinets:这取决于你想要实现什么。如果要获取与float0.30000000000000004 .as_integer_ratio()--内部表示可以看作是用科学计数法写成的二进制数)对应的值,直接传递float即可。如果要获取(天真的)“所见即所得”的十进制比率:30000000000000004/100000000000000000,则需要传递字符串。https://docs.python.org/3/tutorial/floatingpoint.html - jfs
8
请确认:这不是一个四舍五入误差。一个浮点数 0.30000000000000004 和一个由 '0.30000000000000004' 表示的数字是不同的(因为后者不能精确地表示为一个浮点数)。换句话说,十进制数并没有在此引入误差,而是相反的,str() 改变了浮点数的值。只是恰好 str() 产生的误差可能对于一些关于浮点数的朴素观点是可取的。 - jfs
1
请注意,format(0.012345, ".2g") == 0.012 - 保留了三个小数位,而 format(0.12345, ".2g") == 0.12。应该使用 f 格式化程序:format(0.012345, ".2f") == 0.01 - Chris
1
@Chris:不是 f,在这里会出错。尝试使用 1.23e-20 - jfs
显示剩余4条评论

35
我建议这个。
>>> a = 2.111111
>>> a
2.1111110000000002
>>> str(a)
'2.111111'
>>> decimal.Decimal(str(a))
Decimal('2.111111')

4
这种方法能得出最合理的结果。在Python 2.7中直接从浮点数构建Decimal类型可能会产生意想不到的结果,如果你以前玩过浮点数,那就一点也不意外了... - Nick Chammas
2
需要注意的一点是(正如在使用repr()的答案评论中提到的),这里的str()将限制到17个小数位,例如Decimal(str(0.12345678901234567890))的结果为Decimal('0.12345678901234568') - Gary
那不适用于:8.90.7 --> >>> str(8.90.7) '6.2299999999999995' - Denny Weinberg

32
你在问题中提到:

有没有人能够建议一种好的方法,将浮点数转换为Decimal并且保留用户输入的值

但每次用户输入一个值时,它都是作为字符串而不是浮点数输入的。你需要直接将其转换为Decimal,这样就不会失去精度。请勿在任何地方将其转换为浮点数。

很不幸,它在数据库中以浮点数的形式存储,我无法更改模式。 - Kozyarchuk
15
如果你已经失去了“用户输入的值”,那么你就无法再找回它。你所能做的只是进行任意舍入并希望能得到正确的结果。Python对于Decimal类型的正确处理能够把这个问题提醒给你,让你现在就能够理解问题,而不是后面遇到奇怪的错误。 - bobince
2
@Kozyarchuk:我知道你说过不能改变模式,但是你可能想考虑将其更改为十进制类型(在某个时候),因为这在未来可能会变得更加棘手。 - Jeremy Cantrell

8

您可以通过以下方式将数值转换并量化为小数点后五位:

Decimal(float).quantize(Decimal("1.00000"))

类型错误:不支持从类型到Decimal的转换。 - undefined
把你的浮标放在那边 - undefined

5

Python可以从浮点数创建Decimal。你只需要将其先转换为字符串。但是,当进行字符串转换时,不会出现精度损失。你要转换的浮点数本来就没有那种精度。(否则,你就不需要Decimal)

我认为这里的混淆在于我们可以使用十进制格式创建浮点数字面量,但一旦解释器消耗该字面量,内部表示就变成了浮点数。


5

浮点数的“官方”字符串表示由内置函数repr()提供:

>>> repr(1.5)
'1.5'
>>> repr(12345.678901234567890123456789)
'12345.678901234567'

您可以使用repr()代替格式化字符串,结果不会包含任何不必要的垃圾。

repr函数返回17位有效数字,但通常在第16位和第17位存在误差。所以以下方法行不通。 from decimal import Decimal start = Decimal('500.123456789016') assert start == Decimal(repr(float(start))), "%s != %s " % (start, Decimal(repr(float(start)))) - Kozyarchuk
4
这并不是有效数字的问题,而是十进制数在二进制表示中的问题。你举的例子对于0.6同样不适用。 - davidavr
1
repr 返回最短的表示形式,该形式会得到相同的 IEEE-754 双精度浮点数。 - Pedro Werneck
我应该注意到,在现代版本的Python中,“500.123456789016”测试用例可以正常工作(可能要感谢https://bugs.python.org/issue1580?),因此,如果您绝对不能避免首先使用浮点数,则`Decimal(repr(my_float))`似乎是正确的方法。 - Nickolay

4

今天我遇到了同样的问题/疑问,但迄今为止我对给出的任何答案都不完全满意。问题的核心似乎是:

有人能建议一种好的方法将浮点数转换为十进制数[...],可能限制可支持的有效数字位数吗?

简短的答案/解决方案:可以。

def ftod(val, prec = 15):
    return Decimal(val).quantize(Decimal(10)**-prec)

长答案:

如nosklo所指出的那样,将用户输入转换为浮点数后无法保留其输入。 但是可以使用合理的精度对该值进行四舍五入并将其转换为Decimal。

在我的情况下,我只需要小数点后2到4位数字,但它们需要准确。让我们考虑经典的0.1 + 0.2 == 0.3检查。

>>> 0.1 + 0.2 == 0.3
False

现在让我们通过转换为十进制来完成这个例子(完整示例):
>>> from decimal import Decimal
>>> def ftod(val, prec = 15):   # float to Decimal
...     return Decimal(val).quantize(Decimal(10)**-prec)
... 
>>> ftod(0.1) + ftod(0.2) == ftod(0.3)
True

Ryabchenko Alexander的回答对我非常有帮助。只是缺少一种动态设置精度的方法 - 这是我想要(也许也需要)的功能。 Decimal文档FAQ提供了一个示例,说明如何构造quantize()所需的参数字符串:

>>> Decimal(10)**-4
Decimal('0.0001')

以下是18位数字分隔符后的打印效果(来自于C语言编程,我喜欢Python中的花式表达):

>>> for x in [0.1, 0.2, 0.3, ftod(0.1), ftod(0.2), ftod(0.3)]:
...     print("{:8} {:.18f}".format(type(x).__name__+":", x))
... 
float:   0.100000000000000006
float:   0.200000000000000011
float:   0.299999999999999989
Decimal: 0.100000000000000000
Decimal: 0.200000000000000000
Decimal: 0.300000000000000000

最后,我想知道比较仍然有效的精度是多少:

>>> for p in [15, 16, 17]:
...     print("Rounding precision: {}. Check  0.1 + 0.2 == 0.3  is {}".format(p,
...         ftod(0.1, p) + ftod(0.2, p) == ftod(0.3, p)))
... 
Rounding precision: 15. Check  0.1 + 0.2 == 0.3  is True
Rounding precision: 16. Check  0.1 + 0.2 == 0.3  is True
Rounding precision: 17. Check  0.1 + 0.2 == 0.3  is False

对于最大精度,15似乎是很好的默认值。这在大多数系统上都可以正常工作。如果您需要更多信息,请尝试:

>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

在我的系统上,float类型的尾数有53位,我计算了它所表示的十进制数字的位数:

>>> import math
>>> math.log10(2**53)
15.954589770191003

这告诉我我们用53位可以获得近16位数字。因此,15位对于精度值来说是足够的,并且应该始终有效。16位容易出错,17位肯定会引起麻烦(如上所述)。

无论如何,在我的特定情况下,我只需要2到4位小数精度,但作为一个完美主义者,我很享受调查这个问题 :-)

欢迎任何建议/改进/投诉。


2
当你说“保留用户输入的值”时,为什么不只是将用户输入的值存储为字符串并将其传递给Decimal构造函数呢?

2
主要答案有些误导性。格式化符号 g 忽略小数点后的前导零,所以 format(0.012345, ".2g") 返回 0.012 - 保留三位小数。如果您需要对小数位数有硬性限制,请使用格式化符号fformat(0.012345, ".2f") == 0.01

1

这个问题的“正确”解决方法已经在1990年由Steele和White以及Clinger的PLDI 1990论文中记录下来。

您还可以查看this关于Python Decimal的SO讨论,其中包括我的建议尝试使用类似frap的工具将浮点数转化为有理数。


1
似乎类似的算法后来在Python 3中实现,因此像另一个答案建议的Decimal(repr(float_value))今天可以正常工作。 - Nickolay

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