Python基础:浮点数float精度问题与解决方案 Python基础浮点数float精度问题与解决方案一、开篇一个令人困惑的计算在上一篇文章中我们学习了Python的整数int——精确、无限、完美。今天要讲的浮点数float就没那么完美了。先看一个经典的例子0.10.20.30000000000000004⌨️ 等待——0.1加0.2不是应该等于0.3吗那个尾部的4是怎么回事是Python算错了吗答案是不是Python的错这是所有计算机的通病。这篇文章我就带你搞清楚浮点数的来龙去脉包括为什么会出现精度问题、如何在开发中正确处理浮点数。二、浮点数的基本表示2.1 小数表示# 基本的浮点数写法a3.14b-0.5c2.0d.5# 0.5的简写可以省略小数点前的0e5.# 5.0的简写# 科学计数法f1.5e6# 1.5 × 10⁶ 1500000.0g2.5e-3# 2.5 × 10⁻³ 0.0025h6.02e23# 阿伏伽德罗常数# 下划线分隔Python 3.6pi3.141_592_653_589_793budget1_000_000.502.2 类型确认x3.14print(type(x))# class float# 结果是浮点数的运算print(type(1/2))# class floatprint(type(3.02))# class floatprint(type(2**0.5))# class float Python中任何包含小数点的数字字面量都是float类型。除法运算/的结果也始终是float即使结果恰好是整数如4 / 2结果是2.0而不是2。2.3 浮点数的范围importsys# Python浮点数的范围通常基于IEEE 754双精度print(f浮点数最大值{sys.float_info.max:.10e})# ~1.8e308print(f浮点数最小值正数{sys.float_info.min:.10e})# ~2.2e-308print(f浮点数精度小数位数{sys.float_info.dig})# 15位十进制精度Python的float基于IEEE 754双精度浮点数标准64位提供大约15-17位有效十进制数字的精度。三、浮点数精度问题的源头3.1 二进制不能精确表示某些十进制小数问题的根源在于计算机使用二进制存储数字而很多十进制小数在二进制中是无限循环小数。想一想在十进制中1/3 0.3333…是一个无限循环小数我们只能取近似值。同样的道理在二进制中0.1十进制是一个无限循环小数十进制的 0.1 二进制的 0.0001100110011001100110011... 循环节 0011 无限重复计算机只能存储64位大约相当于二进制53位有效数字所以它必须截断。截断后的值并不精确等于0.1只是非常非常接近0.1。# 验证0.1在内存中的实际值importdecimal# 用高精度decimal显示0.1的实际值print(decimal.Decimal(0.1))# 输出0.1000000000000000055511151231257827021181583404541015625# 这个值不是0.1但非常接近3.2 更多让人惊讶的例子0.10.20.300000000000000040.10.10.10.300000000000000040.1*30.300000000000000040.30.10.2False# 这让人最惊讶1.0/3.00.33333333333333330.1*0.10.010000000000000002⚠️ 这些不是Python独有的问题。JavaScript、Java、C、Go等所有使用IEEE 754浮点数的语言都会出现完全相同的情况。3.3 哪些小数可以精确表示能被精确表示的二进制小数它的分母必须是2的幂次方# 能精确表示的0.51/2✅0.251/4✅0.1251/8✅0.753/4✅0.3753/8✅# 不能精确表示的0.1❌分母有因子50.2❌分母有因子50.3❌分母有因子50.01❌# 验证print(0.50.250.75)# Trueprint(0.10.20.3)# False四、浮点数的运算4.1 基本运算a,b10.5,3.0print(ab)# 13.5print(a-b)# 7.5print(a*b)# 31.5print(a/b)# 3.5print(a//b)# 3.0浮点数的整除结果也是浮点数print(a%b)# 1.5print(a**b)# 1157.62510.5的3次方4.2 浮点数的整除和取余# 浮点数的整除也是向下取整print(7.8//2.5)# 3.0因为2.5*37.52.5*410.0 7.8print(7.8%2.5)# 0.30000000000000077.8 - 2.5*3print(-7.8//2.5)# -4.0向下取整print(-7.8%2.5)# 2.2-7.8 - 2.5*(-4) -7.8 10.0 2.24.3 浮点数比较# ❌ 直接比较相等——可能得到错误结果if0.10.20.3:print(相等)else:print(不相等)# 会输出这个# ✅ 使用容差比较epsilon比较defis_close(a,b,rel_tol1e-9,abs_tol1e-12):判断两个浮点数是否接近即视为相等returnabs(a-b)max(rel_tol*max(abs(a),abs(b)),abs_tol)print(is_close(0.10.2,0.3))# True# ✅ 使用math.isclosePython 3.5importmathprint(math.isclose(0.10.2,0.3))# Trueprint(math.isclose(0.10.2,0.3000000001))# False五、解决浮点数精度问题的四种方案5.1 方案一使用math.isclose()importmath# 判断两个浮点数是否几乎相等a0.10.2b0.3ifmath.isclose(a,b,rel_tol1e-9):print(a和b在误差范围内相等)# 参数说明# rel_tol: 相对容差relative tolerance默认1e-9# abs_tol: 绝对容差absolute tolerance默认0.05.2 方案二使用Decimal精确小数fromdecimalimportDecimal# 注意要从字符串创建Decimal而不是从浮点数# ❌ 错误做法print(Decimal(0.1))# 0.1000000000000000055511151231257827021181583404541015625# ✅ 正确做法print(Decimal(0.1))# 0.1# Decimal支持精确运算aDecimal(0.1)bDecimal(0.2)cabprint(c)# 0.3print(cDecimal(0.3))# True# 设置精度fromdecimalimportgetcontext getcontext().prec50# 设置50位有效数字print(Decimal(1)/Decimal(7))# 0.14285714285714285714285714285714285714285714285714Decimal适合需要精确计算的场景金融、会计、科学计算等。但它的缺点是运算速度比float慢很多。5.3 方案三使用分数FractionfromfractionsimportFraction# 创建分数aFraction(1,3)# 1/3bFraction(2,6)# 2/6自动约分为1/3cFraction(0.25)# 从字符串创建1/4dFraction(3.14)# 3.14 157/50# 分数运算——始终精确print(ab)# 2/3print(a*3)# 1print(ab)# True# 混合运算print(Fraction(0.1)Fraction(0.2))# 3/10print(float(Fraction(0.1)Fraction(0.2)))# 0.3# 分数转小数xFraction(1,7)print(float(x))# 0.14285714285714285Fraction适合有理数运算但在需要大量运算时性能较差。5.4 方案四用整数代替小数# 处理金额时用分作为单位而不是元# 0.1元 10分0.2元 20分0.3元 30分# ❌ 用浮点数可能出错price0.1tax0.2totalpricetaxprint(total)# 0.30000000000000004# ✅ 用整数精确price_cents10# 10分 0.1元tax_cents20# 20分 0.2元total_centsprice_centstax_cents# 30分 0.3元print(f{total_cents/100:.2f}元)# 0.30元 金融系统中普遍使用这个方案——所有金额用最小单位分、厘的整数存储只在最终展示时转换为元。六、浮点数的特殊值6.1 无限大和NaN# 正无穷大pos_inffloat(inf)print(pos_inf)# infprint(pos_inf10**100)# Trueprint(math.isinf(pos_inf))# True# 负无穷大neg_inffloat(-inf)print(neg_inf)# -inf# NaNNot a Numbernanfloat(nan)print(nan)# nanprint(math.isnan(nan))# True# NaN的奇怪行为print(nannan)# FalseNaN不等于任何东西包括它自己print(nan0)# Falseprint(nan0)# False6.2 产生特殊值的运算# 产生无穷大print(1.0/0.0)# ZeroDivisionErrorPython会报错# print(float(inf)) # 创建无穷大的正确方式# 产生NaNimportmathprint(math.sqrt(-1.0))# ValueError# 需要使用cmath进行复数运算importcmathprint(cmath.sqrt(-1))# 1j# 无穷大的运算inffloat(inf)print(inf1)# infprint(inf-inf)# nanprint(inf*0)# nanprint(1.0/inf)# 0.06.3 处理特殊值的工具函数importmathdefdescribe_float(x):描述一个浮点数的状态ifmath.isnan(x):returnNaN非数字elifmath.isinf(x):ifx0:return正无穷大else:return负无穷大elifx0.0:return零else:returnf普通浮点数{x}print(describe_float(3.14))# 普通浮点数3.14print(describe_float(float(nan)))# NaN非数字print(describe_float(float(inf)))# 正无穷大print(describe_float(0.0))# 零七、浮点数格式化输出7.1 控制小数位数pi3.141592653589793# f-string格式化print(f{pi:.2f})# 3.14保留2位小数print(f{pi:.4f})# 3.1416保留4位小数自动四舍五入print(f{pi:.0f})# 3取整print(f{pi:10.3f})# 3.142总宽度10右对齐print(f{pi:010.3f})# 000003.142总宽度10前面补零# format()函数print({:.2f}.format(pi))# 3.14print({:.2f}.format(pi))# 3.14显示正号print({:.2f}.format(-pi))# -3.14# 百分比rate0.8567print(f{rate:.1%})# 85.7%print(f{rate:.2%})# 85.67%# 科学计数法num123456789.0print(f{num:.2e})# 1.23e08print(f{num:.4E})# 1.2346E08# 千分位分隔print(f{num:,.2f})# 123,456,789.007.2 常用的格式化速查value12345.6789# 各种格式化方式print(f默认{value})print(f2位小数{value:.2f})print(f科学计数{value:.2e})print(f百分比{value:.2%})# 注意会乘以100print(f千分位{value:,.2f})print(f右对齐10宽{value:10.1f})print(f左对齐10宽{value:10.1f})print(f居中对齐10宽{value:^10.1f})八、实际开发中的最佳实践8.1 金融计算用DecimalfromdecimalimportDecimal,ROUND_HALF_UPdefcalculate_order_total(unit_price,quantity,tax_rate):计算订单总价精确到分unit_priceDecimal(str(unit_price))quantityDecimal(str(quantity))tax_rateDecimal(str(tax_rate))subtotalunit_price*quantity tax(subtotal*tax_rate).quantize(Decimal(0.01),roundingROUND_HALF_UP)total(subtotaltax).quantize(Decimal(0.01),roundingROUND_HALF_UP)returnfloat(subtotal),float(tax),float(total)subtotal,tax,totalcalculate_order_total(9.99,3,0.06)print(f小计: ¥{subtotal:.2f})print(f税费: ¥{tax:.2f})print(f总计: ¥{total:.2f})8.2 科学计算使用float但注意容差importmathdefsafe_float_equals(a,b):安全的浮点数相等判断returnmath.isclose(a,b,rel_tol1e-9,abs_tol1e-12)# 不应该这样# assert my_calculation() 0.3# 应该这样assertmath.isclose(my_calculation(),0.3)8.3 大数据量的金额用整数classMoney:用整数表示金额分为单位def__init__(self,amount_cents):self.centsamount_centsclassmethoddeffrom_yuan(cls,yuan):returncls(round(yuan*100))defto_yuan(self):returnself.cents/100def__add__(self,other):returnMoney(self.centsother.cents)def__str__(self):returnf¥{self.cents/100:.2f}# 使用priceMoney.from_yuan(99.99)taxMoney.from_yuan(5.99)totalpricetaxprint(total)# ¥105.98九、本篇小结✅ 浮点数精度的核心认知不是Python的bug所有使用IEEE 754标准的语言都一样根源是二进制很多十进制小数在二进制中是无限循环的永远不要直接比较浮点数相等用math.isclose()或容差比较金融计算用DecimalDecimal(0.1)而不是Decimal(0.1)金额可以用整数以分为单位存储避免浮点数了解特殊值inf、-inf、NaN及其行为特点 浮点数精度不是要你害怕使用float而是要你正确地使用它。科学计算、图形渲染、机器学习中的大量运算float完全够用。但涉及钱的计算请用Decimal或整数方案。下一篇我们来看一个较少用到但有时很实用的类型——复数complex。