在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+开始支持。
- a = 3.14
- b = -0.5
- c = 2.0
- d = .5
- e = 5.
- f = 1.5e6
- g = 2.5e-3
- h = 6.02e23
- pi = 3.141_592_653_589_793
- budget = 1_000_000.50
- print(type(3.14))
- print(type(1 / 2))
- print(type(3.0 + 2))
- print(type(2 ** 0.5))
复制代码
Python的float基于IEEE 754双精度浮点数标准(64位),提供大约15到17位有效十进制数字的精度。
- import sys
- print(f'浮点数最大值:{sys.float_info.max:.10e}')
- print(f'浮点数最小值(正数):{sys.float_info.min:.10e}')
- print(f'浮点数精度(小数位数):{sys.float_info.dig}')
复制代码
二、精度问题的源头:二进制无法精确表示部分十进制小数
十进制中的1/3是0.3333…无限循环,只能取近似值;同理,十进制的0.1在二进制中是0.0001100110011001100110011...,循环节0011无限重复。计算机只能存储64位,也就是大约53位有效二进制数字,因此必须截断。截断后的值并不精确等于0.1,只是非常接近0.1。
- import decimal
- 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不能精确表示。
- print(0.5 + 0.25 == 0.75)
- print(0.1 + 0.2 == 0.3)
复制代码
同样的现象不是Python独有。JavaScript、Java、C++、Go等所有使用IEEE 754浮点数的语言,都会出现完全相同的情况。
三、浮点数运算、整除取余与比较陷阱
基本运算中,浮点数的整除结果仍然是浮点数,取余结果也可能带出尾差。
- a, b = 10.5, 3.0
- print(a + b)
- print(a - b)
- print(a * b)
- print(a / b)
- print(a // b)
- print(a % b)
- print(a ** b)
- print(7.8 // 2.5)
- print(7.8 % 2.5)
- print(-7.8 // 2.5)
- 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。直接比较浮点数相等很容易失败,应使用容差比较。
- 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)
- print(is_close(0.1 + 0.2, 0.3))
- import math
- print(math.isclose(0.1 + 0.2, 0.3))
- print(math.isclose(0.1 + 0.2, 0.3000000001))
复制代码
四、四种解决方案
方案一:使用math.isclose()。它适合判断两个浮点数是否在误差范围内相等,Python 3.5+可用。rel_tol是相对容差,默认1e-9;abs_tol是绝对容差,默认0.0。
- import math
- a = 0.1 + 0.2
- b = 0.3
- if math.isclose(a, b, rel_tol=1e-9):
- print('a和b在误差范围内相等')
复制代码
方案二:使用Decimal。关键点是从字符串创建Decimal,而不是从浮点数创建。Decimal(0.1)会保留浮点数实际存储的长尾值;Decimal('0.1')才是精确的十进制0.1。
- from decimal import Decimal
- print(Decimal(0.1))
- print(Decimal('0.1'))
- a = Decimal('0.1')
- b = Decimal('0.2')
- c = a + b
- print(c)
- print(c == Decimal('0.3'))
- from decimal import getcontext
- getcontext().prec = 50
- print(Decimal('1') / Decimal('7'))
复制代码
Decimal适合金融、会计、科学计算等需要精确计算的场景,但运算速度比float慢很多。
方案三:使用Fraction。Fraction适合有理数运算,分数运算始终精确,但大量运算时性能较差。
- from fractions import Fraction
- a = Fraction(1, 3)
- b = Fraction(2, 6)
- c = Fraction('0.25')
- d = Fraction('3.14')
- print(a + b)
- print(a * 3)
- print(a == b)
- print(Fraction('0.1') + Fraction('0.2'))
- print(float(Fraction('0.1') + Fraction('0.2')))
- x = Fraction(1, 7)
- 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。
方案四:用整数代替小数。处理金额时,用分作为单位,而不是用元做浮点运算。
- price = 0.1
- tax = 0.2
- total = price + tax
- print(total)
- price_cents = 10
- tax_cents = 20
- total_cents = price_cents + tax_cents
- print(f'{total_cents / 100:.2f}元')
复制代码
浮点方案会得到0.30000000000000004;整数方案用10分加20分得到30分,最终展示为0.30元。金融系统中普遍使用这个方案:所有金额用最小单位(分、厘)的整数存储,只在最终展示时转换为元。
五、特殊值inf、-inf与NaN
float支持正无穷大、负无穷大和NaN。NaN的特殊行为是:它不等于任何东西,包括它自己。
- import math
- pos_inf = float('inf')
- print(pos_inf)
- print(pos_inf > 10**100)
- print(math.isinf(pos_inf))
- neg_inf = float('-inf')
- print(neg_inf)
- nan = float('nan')
- print(nan)
- print(math.isnan(nan))
- print(nan == nan)
- print(nan > 0)
- 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。
- import math
- import cmath
- print(cmath.sqrt(-1))
- inf = float('inf')
- print(inf + 1)
- print(inf - inf)
- print(inf * 0)
- print(1.0 / inf)
- def describe_float(x):
- if math.isnan(x):
- return 'NaN(非数字)'
- elif math.isinf(x):
- if x > 0:
- return '正无穷大'
- else:
- return '负无穷大'
- elif x == 0.0:
- return '零'
- else:
- return f'普通浮点数:{x}'
- print(describe_float(3.14))
- print(describe_float(float('nan')))
- print(describe_float(float('inf')))
- print(describe_float(0.0))
复制代码
六、浮点数格式化输出
展示层通常需要控制小数位数、百分比、科学计数法、千分位和对齐方式。f-string和format()都可以完成这些格式化。
- pi = 3.141592653589793
- print(f'{pi:.2f}')
- print(f'{pi:.4f}')
- print(f'{pi:.0f}')
- print(f'{pi:10.3f}')
- print(f'{pi:010.3f}')
- print('{:.2f}'.format(pi))
- print('{:+.2f}'.format(pi))
- print('{:+.2f}'.format(-pi))
- rate = 0.8567
- print(f'{rate:.1%}')
- print(f'{rate:.2%}')
- num = 123456789.0
- print(f'{num:.2e}')
- print(f'{num:.4E}')
- print(f'{num:,.2f}')
- value = 12345.6789
- print(f'默认:{value}')
- print(f'2位小数:{value:.2f}')
- print(f'科学计数:{value:.2e}')
- print(f'百分比:{value:.2%}')
- print(f'千分位:{value:,.2f}')
- print(f'右对齐10宽:{value:>10.1f}')
- print(f'左对齐10宽:{value:<10.1f}')
- print(f'居中对齐10宽:{value:^10.1f}')
复制代码
七、实际开发中的处理建议
金融计算用Decimal。下面这个订单总价函数把单价、数量和税率都先转成Decimal,再用quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)保留到分。
- from decimal import Decimal, ROUND_HALF_UP
- def calculate_order_total(unit_price, quantity, tax_rate):
- unit_price = Decimal(str(unit_price))
- quantity = Decimal(str(quantity))
- tax_rate = Decimal(str(tax_rate))
- subtotal = unit_price * quantity
- tax = (subtotal * tax_rate).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
- total = (subtotal + tax).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
- return float(subtotal), float(tax), float(total)
- subtotal, tax, total = calculate_order_total(9.99, 3, 0.06)
- print(f'小计: ¥{subtotal:.2f}')
- print(f'税费: ¥{tax:.2f}')
- print(f'总计: ¥{total:.2f}')
复制代码
科学计算可以使用float,但相等判断要带容差。例如断言计算结果是否接近0.3时,应使用math.isclose,而不是直接写==。
- import math
- def safe_float_equals(a, b):
- return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12)
- assert math.isclose(my_calculation(), 0.3)
复制代码
大数据量的金额可以用整数类封装,内部以分为单位存储。
- 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(99.99)
- tax = Money.from_yuan(5.99)
- total = price + tax
- print(total)
复制代码
八、实践结论
浮点数精度问题的核心认知是:这不是Python的bug,而是IEEE 754双精度表示带来的语言通用现象;根源在于部分十进制小数在二进制中无限循环;不要直接用==判断浮点数相等,应使用math.isclose()或自定义容差比较;金融计算使用Decimal,并且要从字符串构造,如Decimal('0.1')而不是Decimal(0.1);金额可用整数以分为单位存储,避免浮点数累加误差;同时要理解inf、-inf和NaN的行为。float适合科学计算、图形渲染、机器学习等大量数值运算,但涉及钱的计算应优先选择Decimal或整数方案。 |