Python: Float to Decimal conversion and subsequent shifting may give front result

Python: Float to Decimal conversion and subsequent shifting may give wrong result

当我在 Python 中将 float 转换为 decimal.Decimal 并随后调用 Decimal.shift 时,根据浮点数,它可能会给我完全错误和意外的结果。为什么会这样?

转换 123.5 并移动它:

from decimal import Decimal

a = Decimal(123.5)
print(a.shift(1))  # Gives expected result

上面的代码打印了 1235.0 的预期结果。

如果我改为转换并移动 123.4:

from decimal import Decimal

a = Decimal(123.4)
print(a.shift(1))  # Gives UNexpected result

它给了我 3.418860808014869689941406250E-18(大约 0),这是完全出乎意料和错误的。

为什么会这样?


注意: 由于浮点数在内存中的表示,我理解浮点不精确。但是,我无法解释为什么这会给我这样一个完全错误的结果。

编辑: 是的,通常最好不要将浮点数转换为小数,而是将字符串转换为小数。但是,这不是我的问题的重点。我想了解为什么 float 转换后的移位会给出这样一个完全错误的结果。所以如果我 print(Decimal(123.4)) 它给出 123.40000000000000568434188608080148696899414062 所以在移动之后我希望它是 1234.0000000000000568434188608080148696899414062 而不是几乎为零。

您需要更改 Decimal 构造函数输入以使用字符串而不是浮点数。

a = Decimal('123.5')
print(a.shift(1))

a = Decimal('123.4')
print(a.shift(1))

a = Decimal(str(123.5))
print(a.shift(1))

a = Decimal(str(123.4))
print(a.shift(1))

输出将符合预期。

>>> 1235.0
>>> 1234.0

小数实例可以从整数、字符串、浮点数或元组构造。从整数或浮点数构造执行该整数或浮点数的值的精确转换。

对于浮点数,Decimal 调用 Decimal.from_float()

Note that Decimal.from_float(0.1) is not the same as Decimal('0.1'). Since 0.1 is not exactly representable in binary floating point, the value is stored as the nearest representable value which is 0x1.999999999999ap-4. The exact equivalent of the value in decimal is 0.1000000000000000055511151231257827021181583404541015625.

在内部,Python 十进制库将浮点数转换为两个整数,分别表示生成浮点数的分数的分子和分母。

n, d = abs(123.4).as_integer_ratio()

然后计算分母的位长,即用二进制表示数字所需的位数。

k = d.bit_length() - 1

然后从那里的位长k通过分子*5乘以分母的位长的幂来记录十进制数的系数。

coeff = str(n*5**k)

结果值用于使用此值创建一个新的 Decimal 对象,其构造函数参数为 signcoefficientexponent

对于浮点数 123.5 这些值是

>>> 1 1235 -1

对于浮点数 123.4 这些值是

1 123400000000000005684341886080801486968994140625 -45

到目前为止,没有任何问题。

但是,当您调用 shift 时,Decimal 库必须根据您指定的 shift 来计算用零填充数字的量。要在内部执行此操作,需要精度减去系数的长度。

amount_to_pad = context.prec - len(coeff)

默认精度仅为 28,并且使用像 123.4 这样的浮点数,系数变得比上面提到的默认精度长得多。这会产生负数以用零填充,并使数字非常小,如您所指出的。

解决此问题的一种方法是将精度提高到指数的长度 + 您开始使用的数字的长度 (45 + 4)。

from decimal import Decimal, getcontext

getcontext().prec = 49

a = Decimal(123.4)
print(a)
print(a.shift(1))

>>> 123.400000000000005684341886080801486968994140625
>>> 1234.000000000000056843418860808014869689941406250

shift 的文档暗示精度对于此计算很重要:

The second operand must be an integer in the range -precision through precision.

然而,它并没有解释这个警告,因为浮点数不能很好地适应内存限制。

我希望这会引发某种错误并提示您更改输入或提高精度,但至少您知道!

@MarkDickinson 在上面的评论中指出,您可以查看此 Python 错误跟踪器以获取更多信息:https://bugs.python.org/issue7233