python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python浮点数float精度问题

Python基础指南之浮点数float精度问题及解决方案

作者:星河耀银海

这篇文章主要为大家详细介绍了Python中浮点数float的常见精度问题及解决方案,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

一、开篇:一个令人困惑的计算

在上一篇文章中,我们学习了Python的整数int——精确、无限、完美。今天要讲的浮点数float,就没那么"完美"了。

先看一个经典的例子:

>>> 0.1 + 0.2
0.30000000000000004

等待——0.1加0.2不是应该等于0.3吗?那个尾部的4是怎么回事?是Python算错了吗?

答案是:不是Python的错,这是所有计算机的通病。这篇文章,我就带你搞清楚浮点数的来龙去脉,包括为什么会出现精度问题、如何在开发中正确处理浮点数。

二、浮点数的基本表示

2.1 小数表示

# 基本的浮点数写法
a = 3.14
b = -0.5
c = 2.0
d = .5        # 0.5的简写(可以省略小数点前的0)
e = 5.        # 5.0的简写

# 科学计数法
f = 1.5e6     # 1.5 × 10⁶ = 1500000.0
g = 2.5e-3    # 2.5 × 10⁻³ = 0.0025
h = 6.02e23   # 阿伏伽德罗常数

# 下划线分隔(Python 3.6+)
pi = 3.141_592_653_589_793
budget = 1_000_000.50

2.2 类型确认

x = 3.14
print(type(x))   # <class 'float'>

# 结果是浮点数的运算
print(type(1 / 2))   # <class 'float'>
print(type(3.0 + 2)) # <class 'float'>
print(type(2 ** 0.5))# <class 'float'>

Python中,任何包含小数点的数字字面量都是float类型。除法运算 / 的结果也始终是float,即使结果恰好是整数(如 4 / 2 结果是 2.0 而不是 2)。

2.3 浮点数的范围

import sys

# Python浮点数的范围(通常基于IEEE 754双精度)
print(f'浮点数最大值:{sys.float_info.max:.10e}')  # ~1.8e308
print(f'浮点数最小值(正数):{sys.float_info.min:.10e}')  # ~2.2e-308
print(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在内存中的实际值
import decimal

# 用高精度decimal显示0.1的实际值
print(decimal.Decimal(0.1))
# 输出:0.1000000000000000055511151231257827021181583404541015625
# 这个值不是0.1!但非常接近

3.2 更多让人惊讶的例子

>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 * 3
0.30000000000000004

>>> 0.3 == 0.1 + 0.2
False          # 这让人最惊讶!

>>> 1.0 / 3.0
0.3333333333333333

>>> 0.1 * 0.1
0.010000000000000002

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

3.3 哪些小数可以精确表示

能被精确表示的二进制小数,它的分母必须是2的幂次方:

# 能精确表示的
0.5 = 1/2          ✅
0.25 = 1/4         ✅
0.125 = 1/8        ✅
0.75 = 3/4         ✅
0.375 = 3/8        ✅

# 不能精确表示的
0.1                ❌(分母有因子5)
0.2                ❌(分母有因子5)
0.3                ❌(分母有因子5)
0.01               ❌
# 验证
print(0.5 + 0.25 == 0.75)           # True
print(0.1 + 0.2 == 0.3)             # False!

四、浮点数的运算

4.1 基本运算

a, b = 10.5, 3.0

print(a + b)     # 13.5
print(a - b)     # 7.5
print(a * b)     # 31.5
print(a / b)     # 3.5
print(a // b)    # 3.0(浮点数的整除结果也是浮点数)
print(a % b)     # 1.5
print(a ** b)    # 1157.625(10.5的3次方)

4.2 浮点数的整除和取余

# 浮点数的整除也是向下取整
print(7.8 // 2.5)    # 3.0(因为2.5*3=7.5,2.5*4=10.0 > 7.8)
print(7.8 % 2.5)     # 0.3000000000000007(7.8 - 2.5*3)

print(-7.8 // 2.5)   # -4.0(向下取整)
print(-7.8 % 2.5)    # 2.2(-7.8 - 2.5*(-4) = -7.8 + 10.0 = 2.2)

4.3 浮点数比较

# ❌ 直接比较相等——可能得到错误结果
if 0.1 + 0.2 == 0.3:
    print('相等')
else:
    print('不相等')  # 会输出这个!

# ✅ 使用容差比较(epsilon比较)
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))  # True

# ✅ 使用math.isclose(Python 3.5+)
import math
print(math.isclose(0.1 + 0.2, 0.3))          # True
print(math.isclose(0.1 + 0.2, 0.3000000001)) # False

五、解决浮点数精度问题的四种方案

5.1 方案一:使用math.isclose()

import math

# 判断两个浮点数是否"几乎相等"
a = 0.1 + 0.2
b = 0.3

if math.isclose(a, b, rel_tol=1e-9):
    print('a和b在误差范围内相等')

# 参数说明
# rel_tol: 相对容差(relative tolerance),默认1e-9
# abs_tol: 绝对容差(absolute tolerance),默认0.0

5.2 方案二:使用Decimal(精确小数)

from decimal import Decimal

# 注意:要从字符串创建Decimal,而不是从浮点数!
# ❌ 错误做法
print(Decimal(0.1))
# 0.1000000000000000055511151231257827021181583404541015625

# ✅ 正确做法
print(Decimal('0.1'))
# 0.1

# Decimal支持精确运算
a = Decimal('0.1')
b = Decimal('0.2')
c = a + b
print(c)         # 0.3
print(c == Decimal('0.3'))  # True

# 设置精度
from decimal import getcontext
getcontext().prec = 50  # 设置50位有效数字
print(Decimal('1') / Decimal('7'))
# 0.14285714285714285714285714285714285714285714285714

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

5.3 方案三:使用分数(Fraction)

from fractions import Fraction

# 创建分数
a = Fraction(1, 3)     # 1/3
b = Fraction(2, 6)     # 2/6,自动约分为1/3
c = Fraction('0.25')   # 从字符串创建:1/4
d = Fraction('3.14')   # 3.14 = 157/50

# 分数运算——始终精确
print(a + b)           # 2/3
print(a * 3)           # 1
print(a == b)          # True

# 混合运算
print(Fraction('0.1') + Fraction('0.2'))  # 3/10
print(float(Fraction('0.1') + Fraction('0.2')))  # 0.3

# 分数转小数
x = Fraction(1, 7)
print(float(x))        # 0.14285714285714285

Fraction适合有理数运算,但在需要大量运算时性能较差。

5.4 方案四:用整数代替小数

# 处理金额时,用"分"作为单位而不是"元"
# 0.1元 = 10分,0.2元 = 20分,0.3元 = 30分

# ❌ 用浮点数(可能出错)
price = 0.1
tax = 0.2
total = price + tax
print(total)  # 0.30000000000000004

# ✅ 用整数(精确)
price_cents = 10     # 10分 = 0.1元
tax_cents = 20       # 20分 = 0.2元
total_cents = price_cents + tax_cents  # 30分 = 0.3元
print(f'{total_cents / 100:.2f}元')  # 0.30元

金融系统中普遍使用这个方案——所有金额用最小单位(分、厘)的整数存储,只在最终展示时转换为元。

六、浮点数的特殊值

6.1 无限大和NaN

# 正无穷大
pos_inf = float('inf')
print(pos_inf)           # inf
print(pos_inf > 10**100) # True
print(math.isinf(pos_inf))  # True

# 负无穷大
neg_inf = float('-inf')
print(neg_inf)           # -inf

# NaN(Not a Number)
nan = float('nan')
print(nan)               # nan
print(math.isnan(nan))   # True

# NaN的"奇怪"行为
print(nan == nan)        # False!(NaN不等于任何东西,包括它自己)
print(nan > 0)           # False
print(nan < 0)           # False

6.2 产生特殊值的运算

# 产生无穷大
print(1.0 / 0.0)         # ZeroDivisionError(Python会报错)
# print(float('inf'))    # 创建无穷大的正确方式

# 产生NaN
import math
print(math.sqrt(-1.0))   # ValueError
# 需要使用cmath进行复数运算
import cmath
print(cmath.sqrt(-1))    # 1j

# 无穷大的运算
inf = float('inf')
print(inf + 1)           # inf
print(inf - inf)         # nan
print(inf * 0)           # nan
print(1.0 / inf)         # 0.0

6.3 处理特殊值的工具函数

import math

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))        # 普通浮点数:3.14
print(describe_float(float('nan')))# NaN(非数字)
print(describe_float(float('inf')))# 正无穷大
print(describe_float(0.0))         # 零

七、浮点数格式化输出

7.1 控制小数位数

pi = 3.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.14
print('{:+.2f}'.format(pi))  # +3.14(显示正号)
print('{:+.2f}'.format(-pi)) # -3.14

# 百分比
rate = 0.8567
print(f'{rate:.1%}')         # 85.7%
print(f'{rate:.2%}')         # 85.67%

# 科学计数法
num = 123456789.0
print(f'{num:.2e}')          # 1.23e+08
print(f'{num:.4E}')          # 1.2346E+08

# 千分位分隔
print(f'{num:,.2f}')         # 123,456,789.00

7.2 常用的格式化速查

value = 12345.6789

# 各种格式化方式
print(f'默认:{value}')
print(f'2位小数:{value:.2f}')
print(f'科学计数:{value:.2e}')
print(f'百分比:{value:.2%}')        # 注意:会乘以100
print(f'千分位:{value:,.2f}')
print(f'右对齐10宽:{value:>10.1f}')
print(f'左对齐10宽:{value:<10.1f}')
print(f'居中对齐10宽:{value:^10.1f}')

八、实际开发中的最佳实践

8.1 金融计算用Decimal

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}')

8.2 科学计算使用float但注意容差

import math

def safe_float_equals(a, b):
    """安全的浮点数相等判断"""
    return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12)

# 不应该这样
# assert my_calculation() == 0.3

# 应该这样
assert math.isclose(my_calculation(), 0.3)

8.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)  # ¥105.98

九、本篇小结

浮点数精度的核心认知:

  1. 不是Python的bug:所有使用IEEE 754标准的语言都一样
  2. 根源是二进制:很多十进制小数在二进制中是无限循环的
  3. 永远不要直接比较浮点数相等:用 math.isclose() 或容差比较
  4. 金融计算用DecimalDecimal('0.1') 而不是 Decimal(0.1)
  5. 金额可以用整数:以"分"为单位存储,避免浮点数
  6. 了解特殊值:inf、-inf、NaN及其行为特点

浮点数精度不是要你"害怕"使用float,而是要你"正确地"使用它。科学计算、图形渲染、机器学习中的大量运算,float完全够用。但涉及"钱"的计算,请用Decimal或整数方案。下一篇我们来看一个较少用到但有时很实用的类型——复数complex。

以上就是Python基础指南之浮点数float精度问题及解决方案的详细内容,更多关于Python浮点数float精度问题的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文