Python 里的 float 看似简单,实际用起来却经常踩坑:取整结果与预期不符、0.1+0.2 得到一长串尾数、比较两个“相等”的小数却不成立。这些问题一半来自对 float 内置方法不熟悉,一半来自二进制存储带来的精度限制。下面按“内置方法—math 工具函数—精度问题处置”三层来梳理,代码均可直接运行验证。
一、float 的四个内置实例方法
float 内置方法不需要导入任何模块,直接用“浮点对象.方法()”调用,主要覆盖属性判断与格式转换。
1. is_integer():判断小数部分是否为 0
返回布尔值,用于区分“整数形态的浮点数”和真正带小数的浮点数。数据格式化输出时常用它决定是否去掉“.0”后缀。
- num1 = 10.0
- print(num1.is_integer()) # True
- num2 = 10.5
- print(num2.is_integer()) # False
- num3 = -5.0
- print(num3.is_integer()) # True
复制代码
2. as_integer_ratio():返回最简分数元组
返回 (分子, 分母),分母恒为正整数。对有限二进制可表示的值能得到精确分数;对 0.1、0.3 这类无法被二进制精确表示的十进制小数,返回的是其存储近似值的分数形式,不能当作数学意义上的精确分数使用。
- print((0.5).as_integer_ratio()) # (1, 2)
- print((3.0).as_integer_ratio()) # (3, 1)
复制代码
3. hex():输出十六进制浮点字符串
结果为“符号 + 0x + 尾数 + p + 以 2 为底的指数”,可以用 float.fromhex() 反向解析回来,适合需要精确往返序列化的场景。
- print((10.0).hex()) # 0x1.4p+3
- print((-0.5).hex()) # -0x1.0p-1
复制代码
4. conjugate():返回共轭值
普通浮点数可视为虚部为 0 的复数,因此该方法原样返回自身;对复数对象调用则只翻转虚部符号。
- print((5.2).conjugate()) # 5.2
- print((3.14 + 2.71j).conjugate()) # (3.14-2.71j)
复制代码
二、取整与四舍五入:round 与 math 三个函数
这几个函数最容易混用,行为差异集中在负数和“0.5 边界”上。
round() 是内置函数,一个参数时四舍五入到整数,两个参数时保留指定小数位。它的舍入采用“向偶数靠拢”的规则,2.5 得 2、3.5 得 4,并不是简单的“见 5 进 1”;另外受存储精度影响,3.145 保留两位并不一定得到 3.15。
- print(round(3.4)) # 3
- print(round(3.5)) # 4
- print(round(2.5)) # 2(向偶数舍入)
- print(round(3.14159, 2)) # 3.14
- print(round(3.145, 2)) # 3.14,3.145 实际存储为近似值
复制代码
math.floor() 返回小于等于该值的最大整数,math.ceil() 返回大于等于该值的最小整数,math.trunc() 直接截断小数部分、向零取整。三者对负数结果不同,业务中涉及金额、分页、坐标换算时必须明确取整方向。
- import math
- print(math.floor(3.9)) # 3
- print(math.floor(-3.1)) # -4
- print(math.ceil(3.1)) # 4
- print(math.ceil(-3.9)) # -3
- print(math.trunc(3.9)) # 3
- print(math.trunc(-3.9)) # -3
复制代码
三、特殊值判断:NaN 与无穷大
无效运算(如对负数开平方、非法计算)会产生 NaN,溢出型运算会产生 inf,这类值参与比较时几乎全为 False,必须先判定再入后续逻辑。math.isnan() 判 NaN,math.isinf() 判正负无穷,math.isfinite() 判是否为正常有限数,一次排除两类异常。
- import math
- print(math.isnan(float('nan'))) # True
- print(math.isnan(3.14)) # False
- print(math.isinf(float('inf'))) # True
- print(math.isinf(-float('inf'))) # True
- print(math.isfinite(3.14)) # True
- print(math.isfinite(float('nan'))) # False
复制代码
四、math 运算函数补充
math 模块提供了平方根、幂、三角函数、对数等运算,其中 math.pow() 与 ** 语义接近,但走的是浮点运算路径,配合 math.pi 计算三角函数时角度必须以弧度为单位。
- import math
- print(math.sqrt(16.0)) # 4.0
- print(math.pow(2.0, 3.0)) # 8.0
- print(math.sin(math.pi / 2)) # 1.0
- print(math.cos(math.pi)) # -1.0
- print(math.log(10.0)) # 2.302...
- print(math.log10(100.0)) # 2.0
复制代码
五、精度误差的成因与三种处置方案
0.1 与 0.2 在二进制下都是无限循环小数,只能存近似值,相加后误差被放大,于是出现 0.30000000000000004。而 a == 0.1 之所以为 True,是因为两边的近似值恰好一致,并不代表数值精确。判断错误往往不出现在打印结果上,而是出现在金额累计、库存校验、循环终止条件里。
- a = 0.1
- b = 0.2
- print(a + b) # 0.30000000000000004
- print(a == 0.1) # True
复制代码
方案一:decimal 高精度十进制运算。传入字符串构造 Decimal,避免先用 float 污染精度;通过 getcontext().prec 设定有效位数,适用于金融结算、计费、税额计算等不允许误差累积的场景。
- from decimal import Decimal, getcontext
- getcontext().prec = 2
- a = Decimal('0.1')
- b = Decimal('0.2')
- print(a + b) # 0.30
- print(a == Decimal('0.1')) # True
复制代码
方案二:fractions 分数精确表示。把数值转成有理数参与运算,中间结果不会引入误差,需要输出时再转 float。
- from fractions import Fraction
- a = Fraction('0.1')
- b = Fraction('0.2')
- print(a + b) # 3/10
- print(float(a + b)) # 0.3
复制代码
方案三:差值阈值比较。普通计算场景不必引入 Decimal,只需把相等判断改成“差值绝对值小于极小值”,常用阈值取 1e-9。这是排查浮点比较失效问题最轻量的改法。
- a = 0.1
- b = 0.2
- epsilon = 1e-9
- print(abs(a + b - 0.3) < epsilon) # True
复制代码
六、选型建议
内置方法负责 float 自身的属性判断与格式转换,math 负责取整、异常值识别与数值运算,两者构成日常处理的主要工具集。是否引入精度方案,取决于业务是否容忍误差:展示型、统计型计算用阈值比较即可;涉及金额、累计求和、跨系统对账时,应统一改用 Decimal 并以字符串构造;需要保留精确有理数关系的中间计算,可用 Fraction。取整方向、舍入规则(round 向偶数舍入)以及特殊值判断这三处,是最容易在生产环境暴露问题的地方,写代码时应显式选择函数而不是依赖隐含行为。 |