请教一个 Python 浮点数的小问题

2022-02-06 01:42:08 +08:00
 knowckx

v1 = 2.2 * 3 # 6.6
v2 = 3.3 * 2 # 6.6
print(v1, v2, v1==v2, v1<=v2, v1>=v2)

输出结果是:
6.6000000000000005 6.6 False False True

这个结果惊讶到我了,没想到这里也会有坑。
所以浮点数比较的正确方式是?

5030 次点击
所在节点    Python
60 条回复
knowckx
2022-02-06 13:04:01 +08:00
@ipwx 首先感谢回复,其实我也算科班,考的 408 ,985 硕毕业,计组刷过没有 5 遍也有 3 遍,IEEE 754 的题更是做到烂
但是我不认为您这样上来就用"科班"来打标签并 diss 别人是什么好行为。
计科的学生学完 IEEE 754 上机后就不会踩浮点数==的坑啦?真不见得。

下面回到就事论事,我再次搜集了些资料,补充一下自己的看法
```
浮点数比较的正确方法:

a == b 应该是 abs(a - b) < epsilon # 这个没问题,或者使用 math.isclose
a <= b 应该是 a < b + epsilon # 这个也没问题,我想了下,这个应该算是“最佳实践”,感谢分享
a < b 这个倒可以直接 a < b # 理论上没问题,但是我感觉会很多人会踩坑,下面示例
```

def CompareTwoFloat(v1 :float, v2 :float):
if v1 < v2:
pass # 小于的情况,啪啦啪啦写代码
elif v1 > v2:
pass # 大于的情况,啪啦啪啦写代码
else:
pass # 等于的情况,啪啦啪啦写代码

v1 = 2.2 * 3 # 6.6
v2 = 3.3 * 2 # 6.6
CompareTwoFloat(v1, v2)

一个不小心就会踩坑,实际上走分支 1 ,
实际上必须先判断 isclose 去掉 equal 的情况再比较大于小于,真是坑死了
knowckx
2022-02-06 13:07:29 +08:00
@v2exblog 这情况直接用 decimal 包会好一点吧……
joApioVVx4M4X6Rf
2022-02-06 13:08:49 +08:00
@knowckx 是的,技术上可以选择更高精度的运算类型。业务上简单的直接 round 也行
knowckx
2022-02-06 13:09:53 +08:00
@v2exblog 嗯……反正别用自带的浮点数,就是个白板
knowckx
2022-02-06 13:32:39 +08:00
我刚思考了下,得出一个看上去奇怪的结论:
a <= b 应该是 a < b + epsilon
a < b 应该也是 a < b + epsilon
GeruzoniAnsasu
2022-02-06 13:55:42 +08:00
ipwx
2022-02-06 15:06:44 +08:00
@knowckx 其实主要问题是你那个例子里面是要条件完全闭合。。。

a < b 肯定是 a < b 没错,+epsilon 放进你那个例子还是会出问题。

所以只不过是涉及到 == 判断,你需要额外特殊第一优先处理罢了。。。
ipwx
2022-02-06 15:07:43 +08:00
"a < b 肯定是 a < b 没错,+epsilon 放进你那个例子还是会出问题。"

不信你代入一下

if a < b + epsilon:
...
elif a > b - epsilon:
...
else:
...

你会发现比起你的例子还是有可能运行到 else ,你在这个例子完全不可能运行 else 。gg
akazure
2022-02-06 15:25:25 +08:00
Python 有 Fraction 标准库,可以用分子分母算
https://docs.python.org/zh-cn/3/library/fractions.html
leimao
2022-02-06 15:48:39 +08:00
哈哈,多踩几次坑就记住了 。
wangnimabenma
2022-02-06 15:57:00 +08:00
电脑没法用有限的空间存储无限的数据

1. 用高精度库 (还是会有精度丢失只是小到无法察觉或者是说可有可无)
2. 用分数表示
Jooooooooo
2022-02-06 16:20:45 +08:00
因为计算机只有二进制, 十进制下看起来有限的小数, 二进制下表达不了, 十进制 1/3 也搞不定.
des
2022-02-06 16:26:28 +08:00
楼上基本都说清楚了,我再补充一点
精度损失基本都发生在“进制转换”的过程中
举个栗子:“1 小时 20 分钟”转换成“多少小时”的数字,应该写成多少? 1.33333333 是不是?
反向的话也是同理,计算机是二进制,钟表分和秒是 60 进制

另一个问题来了,为什么不写成分数形式?
两个原因,1 、很多时候不需要那么高精度; 2 、计算起来性能太差,也费存储空间

顺带再科普一个知识,浮点数做加减乘除是不会损失精度的,提前是使用得当
des
2022-02-06 16:29:42 +08:00
@des 说错,乘除不算
niqy1988
2022-02-06 16:34:22 +08:00
@knowckx
应该是 a < b 写作 a < b - epsilon
whusnoopy
2022-02-06 17:11:11 +08:00
@knowckx #21

歪个楼讨论下「科班」的问题,非针对楼主,请不用在意,只是有感而发

是不是正儿八经的相关专业就叫科班?

如果我不学无术啥也没学挂科挂到退学,这种大家应该都不会认可是科班

那如果我是相关专业且是做题家,考试分数都挺高,但动手实践各种坑,这种怎么说?不确定现在学校里都是怎样,反正我上学那会(也是 985 本硕计算机专业),班上总有几个纯考试分不错,但真正代码都不一定会写,这种算科班么?

如果我不是相关专业,但系统性学习和理解并能实践相关知识,这种虽然没有对应文凭,是不是也可以算科班?
12101111
2022-02-06 17:50:51 +08:00
我发现 rust 压根不让这么乘

fn main() {
assert_eq!(2.2*3, 3.3*2);
}

error[E0277]: cannot multiply `{float}` by `{integer}`
--> src/main.rs:2:19
|
2 | assert_eq!(2.2*3, 3.3*2);
| ^ no implementation for `{float} * {integer}`
|
= help: the trait `Mul<{integer}>` is not implemented for `{float}`

error[E0277]: cannot multiply `{float}` by `{integer}`
--> src/main.rs:2:26
|
2 | assert_eq!(2.2*3, 3.3*2);
| ^ no implementation for `{float} * {integer}`
|
= help: the trait `Mul<{integer}>` is not implemented for `{float}`

For more information about this error, try `rustc --explain E0277`.

如果改成这样
fn main() {
assert_eq!(2.2*3.0, 3.3*2.0);
}

thread 'main' panicked at 'assertion failed: `(left == right)`
left: `6.6000000000000005`,
right: `6.6`', src/main.rs:2:5
0attocs
2022-02-06 18:18:16 +08:00
@des #22 浮点加减运算怎么可能不会损失精度,运气好的话 relative error 是 bounded by epsilon/2 。运气不好的话很多加减运算本身可以是 ill-condition 的,遇上浮点加减运算的 rounding error 之后 relative error 会高得离谱。比如考虑实数:

x = 1 + 2^-52 + 2^-53 + 2^-54
y = 1 + 2^-54

实数减法 x - y 的结果是 2^-52 + 2^-53 = 3 * 2^-53 ,但 64 位浮点数减法 fp(x) fp(-) fp(y) 的结果是 2^-51 = 4 * 2^-53 ,relative error 是夸张的 |(4^-53 - 3 * 2^-53) / 3 * 2^-53| = 1/3 。
des
2022-02-06 18:27:12 +08:00
@0attocs 所以才说“前提得正确使用”啊
0attocs
2022-02-06 18:42:13 +08:00
@knowckx #19 需要区分<和<=时的一种做法是考虑用 a <= b + delta 代替 a < b ,但一般只有在为某个涉及浮点数比较的分析而定义语义时需要考虑吧。一般情况下更应该考虑的是问题本身是否是 well-conditioned 的以及算法+实现本身是否 stable 。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/832021

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX