在Python开发中,0.1 + 0.2 的结果是 0.30000000000000004 而不是 0.3,这并非Python的bug,而是所有采用IEEE 754双精度浮点数标准的编程语言(JavaScript、Java、C++等)共有的特性。本文将从浮点数精度问题的根源入手,总结四种成熟的解决方案,并给出金融计算、科学计算等场景的最佳实践。
一、精度问题的根源
计算机使用二进制存储数字,而十进制的0.1在二进制中是无限循环小数:0.0001100110011...(循环节0011)。由于float类型基于64位存储(有效数字约53位),只能截断近似表示。验证如下:- import decimal
- print(decimal.Decimal(0.1))
- # 输出:0.1000000000000000055511151231257827021181583404541015625
复制代码 能被精确表示的十进制小数,其分母必须是2的幂次方,例如0.5(1/2)、0.25(1/4)、0.125(1/8)。而0.1、0.2、0.3分母含有因子5,无法精确表示。
二、四种解决方案
方案1:使用 math.isclose() 进行容差比较
永远不要用 == 直接比较浮点数,应使用相对容差或绝对容差。Python 3.5+ 提供了 math.isclose():- import math
- a = 0.1 + 0.2
- b = 0.3
- print(math.isclose(a, b, rel_tol=1e-9)) # True
- # 自定义容差函数(原理相同)
- def is_close(a, b, rel_tol=1e-9, abs_tol=1e-12):
- return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
复制代码 rel_tol为相对容差(默认1e-9),abs_tol为绝对容差(默认0.0)。此方案适合科学计算、图形渲染等不需要绝对精确的场景。
方案2:使用 Decimal 精确小数
Decimal 可以精确表示十进制小数,但必须从字符串创建,而非float:- from decimal import Decimal, getcontext
- # 正确做法:从字符串创建
- a = Decimal('0.1')
- b = Decimal('0.2')
- print(a + b) # 0.3
- print(a + b == Decimal('0.3')) # True
- # 设置全局精度
- getcontext().prec = 50
- print(Decimal('1') / Decimal('7')) # 0.142857...(50位)
复制代码 注意:Decimal(0.1) 会先把float转成近似值再创建,导致精度丢失。Decimal运算速度比float慢,适合金融、会计等需要精确计算金额的场景。
方案3:使用 Fraction 分数
Fraction 以有理数形式存储,运算始终精确:- from fractions import Fraction
- a = Fraction(1, 3)
- b = Fraction('0.25') # 自动转为1/4
- print(a + b) # 7/12
- print(Fraction('0.1') + Fraction('0.2')) # 3/10
- # 转回浮点数
- print(float(Fraction('0.1') + Fraction('0.2'))) # 0.3
复制代码 Fraction适合有理数运算,但大量运算时性能较差。
方案4:用整数代替小数(金融系统常用)
将所有金额以最小单位(如分)的整数存储,仅在显示时转换:- class Money:
- def __init__(self, amount_cents):
- self.cents = amount_cents
- @classmethod
- def from_yuan(cls, yuan):
- return cls(round(yuan * 100))
- def to_yuan(self):
- return self.cents / 100
- def __add__(self, other):
- return Money(self.cents + other.cents)
- def __str__(self):
- return f'¥{self.cents / 100:.2f}'
- price = Money.from_yuan(9.99)
- tax = Money.from_yuan(0.60)
- total = price + tax
- print(total) # ¥10.59
复制代码
三、浮点数的特殊值处理
正无穷大、负无穷大和NaN(Not a Number)在运算中可能产生,需使用 math.isinf() 和 math.isnan() 判断:- import math
- inf = float('inf')
- nan = float('nan')
- print(math.isinf(inf)) # True
- print(math.isnan(nan)) # True
- # NaN的特殊性:与自己不相等
- print(nan == nan) # False
复制代码
四、浮点数格式化输出
使用f-string控制小数位数、千分位分隔等:- pi = 3.141592653589793
- print(f'{pi:.2f}') # 3.14
- print(f'{pi:10.3f}') # ' 3.142'(右对齐,总宽10)
- print(f'{rate:.1%}') # 85.7%(rate=0.8567)
- print(f'{123456789.0:,.2f}') # 123,456,789.00
复制代码
五、实际开发最佳实践
- 金融计算:优先使用 Decimal 或整数(分/厘),搭配 ROUND_HALF_UP 四舍五入。
- 科学计算:float 足够,但比较时使用 math.isclose()。
- 大数据量金额:全部以整数存储,避免 Decimal 性能开销。
总结:浮点数精度问题并非Python独有,理解其二进制表示原理后,应根据场景选择合适方案:比较用 isclose,精确计算用 Decimal 或 Fraction,金融系统用整数。掌握这些技巧,可以避免大多数浮点数陷阱。 |