查看: 366|回复: 0

Python浮点数精度问题与isclose、Decimal方案

[复制链接]
发表于 4 小时前 | 显示全部楼层 |阅读模式
在Python中执行0.1 + 0.2,结果常出现0.30000000000000004;而0.1 + 0.2 == 0.3会返回False。这不是Python的算术错误,而是IEEE 754双精度浮点表示带来的普遍现象。下面围绕float的类型表示、精度来源、比较方式、Decimal/Fraction/整数替代方案、特殊值和格式化输出,按可落地的代码实践重新组织。

一、float的表示范围与类型确认

Python中任何包含小数点的数字字面量都是float;除法/的结果也始终是float,即使结果恰好是整数,例如4 / 2得到2.0而不是2。科学计数法和下划线分隔可用于书写浮点数,下划线分隔从Python 3.6+开始支持。
  1. a = 3.14
  2. b = -0.5
  3. c = 2.0
  4. d = .5
  5. e = 5.
  6. f = 1.5e6
  7. g = 2.5e-3
  8. h = 6.02e23
  9. pi = 3.141_592_653_589_793
  10. budget = 1_000_000.50
  11. print(type(3.14))
  12. print(type(1 / 2))
  13. print(type(3.0 + 2))
  14. print(type(2 ** 0.5))
复制代码

Python的float基于IEEE 754双精度浮点数标准(64位),提供大约15到17位有效十进制数字的精度。
  1. import sys
  2. print(f'浮点数最大值:{sys.float_info.max:.10e}')
  3. print(f'浮点数最小值(正数):{sys.float_info.min:.10e}')
  4. print(f'浮点数精度(小数位数):{sys.float_info.dig}')
复制代码

二、精度问题的源头:二进制无法精确表示部分十进制小数

十进制中的1/3是0.3333…无限循环,只能取近似值;同理,十进制的0.1在二进制中是0.0001100110011001100110011...,循环节0011无限重复。计算机只能存储64位,也就是大约53位有效二进制数字,因此必须截断。截断后的值并不精确等于0.1,只是非常接近0.1。
  1. import decimal
  2. print(decimal.Decimal(0.1))
复制代码

输出为0.1000000000000000055511151231257827021181583404541015625。这个值不是0.1,但非常接近。能被精确表示的二进制小数,其分母必须是2的幂次方:0.5、0.25、0.125、0.75、0.375可以精确表示;0.1、0.2、0.3、0.01不能精确表示。
  1. print(0.5 + 0.25 == 0.75)
  2. print(0.1 + 0.2 == 0.3)
复制代码

同样的现象不是Python独有。JavaScript、Java、C++、Go等所有使用IEEE 754浮点数的语言,都会出现完全相同的情况。

三、浮点数运算、整除取余与比较陷阱

基本运算中,浮点数的整除结果仍然是浮点数,取余结果也可能带出尾差。
  1. a, b = 10.5, 3.0
  2. print(a + b)
  3. print(a - b)
  4. print(a * b)
  5. print(a / b)
  6. print(a // b)
  7. print(a % b)
  8. print(a ** b)
  9. print(7.8 // 2.5)
  10. print(7.8 % 2.5)
  11. print(-7.8 // 2.5)
  12. print(-7.8 % 2.5)
复制代码

其中7.8 // 2.5得到3.0,7.8 % 2.5得到0.3000000000000007;-7.8 // 2.5得到-4.0,-7.8 % 2.5得到2.2。直接比较浮点数相等很容易失败,应使用容差比较。
  1. def is_close(a, b, rel_tol=1e-9, abs_tol=1e-12):
  2.     return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
  3. print(is_close(0.1 + 0.2, 0.3))
  4. import math
  5. print(math.isclose(0.1 + 0.2, 0.3))
  6. print(math.isclose(0.1 + 0.2, 0.3000000001))
复制代码

四、四种解决方案

方案一:使用math.isclose()。它适合判断两个浮点数是否在误差范围内相等,Python 3.5+可用。rel_tol是相对容差,默认1e-9;abs_tol是绝对容差,默认0.0。
  1. import math
  2. a = 0.1 + 0.2
  3. b = 0.3
  4. if math.isclose(a, b, rel_tol=1e-9):
  5.     print('a和b在误差范围内相等')
复制代码

方案二:使用Decimal。关键点是从字符串创建Decimal,而不是从浮点数创建。Decimal(0.1)会保留浮点数实际存储的长尾值;Decimal('0.1')才是精确的十进制0.1。
  1. from decimal import Decimal
  2. print(Decimal(0.1))
  3. print(Decimal('0.1'))
  4. a = Decimal('0.1')
  5. b = Decimal('0.2')
  6. c = a + b
  7. print(c)
  8. print(c == Decimal('0.3'))
  9. from decimal import getcontext
  10. getcontext().prec = 50
  11. print(Decimal('1') / Decimal('7'))
复制代码

Decimal适合金融、会计、科学计算等需要精确计算的场景,但运算速度比float慢很多。

方案三:使用Fraction。Fraction适合有理数运算,分数运算始终精确,但大量运算时性能较差。
  1. from fractions import Fraction
  2. a = Fraction(1, 3)
  3. b = Fraction(2, 6)
  4. c = Fraction('0.25')
  5. d = Fraction('3.14')
  6. print(a + b)
  7. print(a * 3)
  8. print(a == b)
  9. print(Fraction('0.1') + Fraction('0.2'))
  10. print(float(Fraction('0.1') + Fraction('0.2')))
  11. x = Fraction(1, 7)
  12. print(float(x))
复制代码

其中Fraction(2, 6)会自动约分为1/3;Fraction('0.25')得到1/4;Fraction('3.14')得到157/50;Fraction('0.1') + Fraction('0.2')得到3/10。

方案四:用整数代替小数。处理金额时,用分作为单位,而不是用元做浮点运算。
  1. price = 0.1
  2. tax = 0.2
  3. total = price + tax
  4. print(total)
  5. price_cents = 10
  6. tax_cents = 20
  7. total_cents = price_cents + tax_cents
  8. print(f'{total_cents / 100:.2f}元')
复制代码

浮点方案会得到0.30000000000000004;整数方案用10分加20分得到30分,最终展示为0.30元。金融系统中普遍使用这个方案:所有金额用最小单位(分、厘)的整数存储,只在最终展示时转换为元。

五、特殊值inf、-inf与NaN

float支持正无穷大、负无穷大和NaN。NaN的特殊行为是:它不等于任何东西,包括它自己。
  1. import math
  2. pos_inf = float('inf')
  3. print(pos_inf)
  4. print(pos_inf > 10**100)
  5. print(math.isinf(pos_inf))
  6. neg_inf = float('-inf')
  7. print(neg_inf)
  8. nan = float('nan')
  9. print(nan)
  10. print(math.isnan(nan))
  11. print(nan == nan)
  12. print(nan > 0)
  13. print(nan < 0)
复制代码

产生特殊值时要注意:Python中1.0 / 0.0会抛出ZeroDivisionError;math.sqrt(-1.0)会抛出ValueError,需要复数运算时使用cmath.sqrt(-1),结果为1j。无穷大参与运算时,inf + 1得到inf,inf - inf得到nan,inf * 0得到nan,1.0 / inf得到0.0。
  1. import math
  2. import cmath
  3. print(cmath.sqrt(-1))
  4. inf = float('inf')
  5. print(inf + 1)
  6. print(inf - inf)
  7. print(inf * 0)
  8. print(1.0 / inf)
  9. def describe_float(x):
  10.     if math.isnan(x):
  11.         return 'NaN(非数字)'
  12.     elif math.isinf(x):
  13.         if x > 0:
  14.             return '正无穷大'
  15.         else:
  16.             return '负无穷大'
  17.     elif x == 0.0:
  18.         return '零'
  19.     else:
  20.         return f'普通浮点数:{x}'
  21. print(describe_float(3.14))
  22. print(describe_float(float('nan')))
  23. print(describe_float(float('inf')))
  24. print(describe_float(0.0))
复制代码

六、浮点数格式化输出

展示层通常需要控制小数位数、百分比、科学计数法、千分位和对齐方式。f-string和format()都可以完成这些格式化。
  1. pi = 3.141592653589793
  2. print(f'{pi:.2f}')
  3. print(f'{pi:.4f}')
  4. print(f'{pi:.0f}')
  5. print(f'{pi:10.3f}')
  6. print(f'{pi:010.3f}')
  7. print('{:.2f}'.format(pi))
  8. print('{:+.2f}'.format(pi))
  9. print('{:+.2f}'.format(-pi))
  10. rate = 0.8567
  11. print(f'{rate:.1%}')
  12. print(f'{rate:.2%}')
  13. num = 123456789.0
  14. print(f'{num:.2e}')
  15. print(f'{num:.4E}')
  16. print(f'{num:,.2f}')
  17. value = 12345.6789
  18. print(f'默认:{value}')
  19. print(f'2位小数:{value:.2f}')
  20. print(f'科学计数:{value:.2e}')
  21. print(f'百分比:{value:.2%}')
  22. print(f'千分位:{value:,.2f}')
  23. print(f'右对齐10宽:{value:>10.1f}')
  24. print(f'左对齐10宽:{value:<10.1f}')
  25. print(f'居中对齐10宽:{value:^10.1f}')
复制代码

七、实际开发中的处理建议

金融计算用Decimal。下面这个订单总价函数把单价、数量和税率都先转成Decimal,再用quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)保留到分。
  1. from decimal import Decimal, ROUND_HALF_UP
  2. def calculate_order_total(unit_price, quantity, tax_rate):
  3.     unit_price = Decimal(str(unit_price))
  4.     quantity = Decimal(str(quantity))
  5.     tax_rate = Decimal(str(tax_rate))
  6.     subtotal = unit_price * quantity
  7.     tax = (subtotal * tax_rate).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
  8.     total = (subtotal + tax).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
  9.     return float(subtotal), float(tax), float(total)
  10. subtotal, tax, total = calculate_order_total(9.99, 3, 0.06)
  11. print(f'小计: ¥{subtotal:.2f}')
  12. print(f'税费: ¥{tax:.2f}')
  13. print(f'总计: ¥{total:.2f}')
复制代码

科学计算可以使用float,但相等判断要带容差。例如断言计算结果是否接近0.3时,应使用math.isclose,而不是直接写==。
  1. import math
  2. def safe_float_equals(a, b):
  3.     return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12)
  4. assert math.isclose(my_calculation(), 0.3)
复制代码

大数据量的金额可以用整数类封装,内部以分为单位存储。
  1. class Money:
  2.     '用整数表示金额(分为单位)'
  3.     def __init__(self, amount_cents):
  4.         self.cents = amount_cents
  5.     @classmethod
  6.     def from_yuan(cls, yuan):
  7.         return cls(round(yuan * 100))
  8.     def to_yuan(self):
  9.         return self.cents / 100
  10.     def __add__(self, other):
  11.         return Money(self.cents + other.cents)
  12.     def __str__(self):
  13.         return f'¥{self.cents / 100:.2f}'
  14. price = Money.from_yuan(99.99)
  15. tax = Money.from_yuan(5.99)
  16. total = price + tax
  17. print(total)
复制代码

八、实践结论

浮点数精度问题的核心认知是:这不是Python的bug,而是IEEE 754双精度表示带来的语言通用现象;根源在于部分十进制小数在二进制中无限循环;不要直接用==判断浮点数相等,应使用math.isclose()或自定义容差比较;金融计算使用Decimal,并且要从字符串构造,如Decimal('0.1')而不是Decimal(0.1);金额可用整数以分为单位存储,避免浮点数累加误差;同时要理解inf、-inf和NaN的行为。float适合科学计算、图形渲染、机器学习等大量数值运算,但涉及钱的计算应优先选择Decimal或整数方案。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-20 14:12 , Processed in 0.025013 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部