Python decimal/float中的邪恶

17 投票
3 回答
6292 浏览
提问于 2025-04-16 07:05

我有一大堆 Python 代码,主要是处理四位小数的数字,但因为各种原因,我只能用 Python 2.4。这个代码做的数学运算很简单,主要是管理信用额度,增加或减少信用。

在代码中,我混用了 float 和 Decimal(MySQLdb 会返回 Decimal 对象来处理 SQL 中的 DECIMAL 类型)。由于使用不当,出现了几种奇怪的错误,经过调查,我发现这些问题的根源在于代码中 float 和 Decimal 的比较。

我遇到的情况是这样的:

>>> from decimal import Decimal
>>> max(Decimal('0.06'), 0.6)
Decimal("0.06")

现在我担心的是,可能无法在代码中找到所有这样的比较情况。(一般程序员会习惯性地写 x > 0,而不是 x > Decimal('0.0000),这很难避免)

我想出了一个解决办法(灵感来自 Python 2.7 中对 decimal 包的改进)。

import decimal
def _convert_other(other):
     """Convert other to Decimal.

     Verifies that it's ok to use in an implicit construction.
     """
     if isinstance(other, Decimal):
         return other
     if isinstance(other, (int, long)):
         return Decimal(other)
     # Our small patch begins
     if isinstance(other, float):
         return Decimal(str(other))
     # Our small patch ends
     return NotImplemented
decimal._convert_other = _convert_other

我在一个很早加载的库中实现了这个解决方案,它会改变 decimal 包的行为,允许在比较之前将 float 转换为 Decimal(这样可以避免 Python 默认的对象比较)。

我特别使用了 "str" 而不是 "repr",因为这样可以修复一些 float 的四舍五入问题。例如:

>>> Decimal(str(0.6))
Decimal("0.6")
>>> Decimal(repr(0.6))
Decimal("0.59999999999999998")

现在我的问题是: 我有没有遗漏什么?这样做安全吗?还是说我可能会破坏什么?(我在想,这个包的作者一定有很强的理由来避免使用 float)

3 个回答

-2

首先,浮点数并不是“邪恶”的东西。你看到的不准确其实是机器误差造成的(也就是说,有限的系统/精度在试图表示一个无限的系统/精度)。

你可以试试用 round( , 2) 这个方法,这样可以把数字四舍五入到小数点后两位,适合用在钱或者信用计算上(因为这就是你需要的精度范围)。

>>> round(0.6, 2)
0.6
>>> round(0.5999998, 2)
0.6
3

避免使用浮点数是有很好的理由的。使用浮点数时,你无法可靠地进行比较,比如 ==、>、< 等,因为会出现浮点数的噪音。每次进行浮点运算时,都会积累这种噪音。一开始可能只是一些很小的数字出现在最后,比如 1.000...002,但最终可能会累积成像 1.0000000453436 这样的大数字。

如果你进行的浮点计算不多,使用 str() 可能会对你有帮助,但如果你进行很多计算,浮点数的噪音最终会变得足够大,以至于 str() 给出的结果会是错误的。

总的来说,如果你满足以下两个条件中的任意一个:

(1) 你进行的浮点计算不多,或者

(2) 你不需要进行像 ==、>、< 这样的比较

那么你可能还好。

如果你想确保没有问题,那就把所有的浮点数代码都去掉。

4

我觉得你应该用 raise NotImplementedError(),而不是 return NotImplemented,这才是正确的开始。

你现在做的事情叫“猴子补丁”,这其实是可以的,只要你知道自己在干什么,明白可能带来的后果,并且能接受这些后果。一般来说,你会把这种做法限制在修复一个bug,或者其他你确定修改行为是正确且向后兼容的情况。

在这个例子中,因为你在修改一个类,所以你可以在使用这个类的地方以外改变它的行为。如果其他库使用了decimal,并且依赖于默认的行为,这可能会导致一些隐蔽的bug。问题是,除非你检查你所有的代码,包括所有的依赖项,否则你根本不知道会出现什么问题,也找不到所有的调用点。

总的来说,做这件事要自己承担风险。

就我个人而言,我觉得修复我所有的代码,添加测试,并让错误的操作变得更难(比如使用包装类或辅助函数)会让我更安心。另一种方法是给你的代码加上补丁,找出所有的调用点,然后再回去修复它们。

补充一下,我想说他们可能避免使用浮点数的原因是,浮点数无法准确表示所有数字,这在处理钱的时候是很重要的。

撰写回答