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
这个结果惊讶到我了,没想到这里也会有坑。
所以浮点数比较的正确方式是?
1
0ZXYDDu796nVCFxq 2022-02-06 01:44:56 +08:00 1
|
2
chevalier 2022-02-06 01:46:54 +08:00
跟 Python 没关系,了解一下浮点数的原理,所有的语言都这样
正确的比较,使用语言自带的浮点库,或者 v1-v2 < 0.0000……01 这样 |
3
knowckx OP |
5
secondwtq 2022-02-06 02:01:33 +08:00
这东西能简单能复杂,看你想要简单的还是要麻烦的
randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition Comparing Floating Point Numbers, 2012 Edition | Random ASCII – tech blog of Bruce Dawson |
6
0ZXYDDu796nVCFxq 2022-02-06 02:04:19 +08:00
@knowckx 哪个语言的浮点数可以直接 ==?
|
7
yaojin 2022-02-06 02:10:39 +08:00 via Android
该睡了,还是说你是海外的,太卷了吧
|
8
lunaticus7 2022-02-06 02:14:52 +08:00 1
https://docs.python.org/3/library/decimal.html
想要精确小数的话可以用 decimal |
9
knowckx OP @gstqc 我试了下 go 可以的
func Test_FloatEqual(t *testing.T) { v1 := 2.2 * 3 v2 := 3.3 * 2 fmt.Println(v1, v2, v1 == v2, v1 <= v2, v1 >= v2) } 输出结果: === RUN Test_FloatEqual 6.6 6.6 true true true --- PASS: Test_FloatEqual (0.00s) PASS |
12
knowckx OP @lunaticus7 这个方式不错,就是麻烦点,要把所有可能要比较的浮点数都用 decimal 转一遍
|
13
iBugOne 2022-02-06 02:48:35 +08:00 via Android 8
@knowckx Go 语言里 2.2*3 这种写法不涉及浮点运算,因为它是一个常量。大部分 C 语言编译器也会做这样的优化,而 Python 是“写啥跑啥”的,所以只有 Python 是真的创建了两个浮点数和两个整数并且做浮点乘法的。
Go 换成这种写法你就发现区别了: c1 := 2.2 c2 := 3.3 v1 := c1 * 3 v2 := c2 * 2 |
14
LeeReamond 2022-02-06 09:03:54 +08:00 1
没有什么简单的办法,首先是类 C 语言的通用问题,其次是 py 里想简写的话,运算符可以重载,但只能发生在对象上,所以还要全局钩子设置对象,很繁琐,不如接受全球程序员都接受的事实。
|
15
ipwx 2022-02-06 10:04:48 +08:00 5
唉,又疯了一个。。。不得不感慨科班还是硬道理。
---- “我理解部分小数无法精确存储” —— 所有不能写成 Sum[2^i] ( i 可为负数)的浮点数都不能精确存储。 “只是其他语言没这么麻烦的” “我试了下 go 可以的” —— 这大概就是 Go 语言被很多人喜欢的原因吧,隐藏了非常多的实践细节。但是在我看来你甚至不知道 Go 语言哪些时候已经帮你包办了,哪些时候需要自己处理,这种不一致性会让人发狂。比如 Go 的协程为了实现真时间片而在代码里面真的插入了一堆别的语言要手动写的 sleep(0),知道这个我是震惊的,这如果是写算法妥妥的浪费了一堆时间啊! 浮点数比较的正确方法: a == b 应该是 abs(a - b) < epsilon a <= b 应该是 a < b + epsilon a < b 这个倒可以直接 a < b ---- @iBugOne 附:其他语言的常量比较结果 2.2 * 3 == 3.3 * 2 JS false https://ideone.com/o297hf PHP false https://ideone.com/HMz6Nl C++ false https://ideone.com/E9YMqN C# false https://ideone.com/tylfdw Java false https://ideone.com/yz7Beu 你看无论编译型还是非编译型,就算是常量它也不应该有这个等号啊。。。。Go 会输出等于我是震惊的。 |
16
ipwx 2022-02-06 10:06:28 +08:00 1
哦对 epsilon 是一个自己可以控制的小数常量,根据业务需求定。比如一般我会取 epsilon = 1e-7 (对 float 也一般有效了)。但是对 double 而言你也许可以使用 epsilon = 1e-12 。但无论如何自己能控制比语言帮你处理(却不知道到底怎么做的)要安全多了。
|
17
rcocco 2022-02-06 10:33:51 +08:00 2
坑在于你输入的值(屏幕上显示的值)和实际值并不一样,当你输入 2.2 的时候,程序使用的实际值是 2.20000000000000017763568394002504646778106689453125 。Python 和很多语言认为这个精确值太长了不方便人类阅读,所以会自作主张在输出时显示为 2.2 。
而你输入 3.3 的时候实际值是 3.29999999999999982236431605997495353221893310546875 。 所以 2.20000000000000017763568394002504646778106689453125 * 3 >= 3.29999999999999982236431605997495353221893310546875 * 2 为 True 没有任何问题,不信你拿计算器敲一遍 假设 Round 表示对你输入的数字取最接近的浮点数, 你输入 2.2*3 == 3.3*2 ,实际进行的是:Round(Round(2.2) * 3) == Round(Round(3.3) * 2) Round 后的结果可能比原来大,也可能小,还可能等。 所以浮点数比较只能作差取绝对值,差小于某个很小的数就认为是相等。 |
18
knowckx OP @iBugOne 感谢回复,我跑了下确实有区别,但是你提到的
2.2*3 是一个常量 我没有理解,google 了下也没搜到什么内容,似乎是 go 对常量表达式有优化 |
19
agagega 2022-02-06 12:44:46 +08:00 1
|
20
joApioVVx4M4X6Rf 2022-02-06 13:02:53 +08:00
从业务角度来说的话,比较的时候,需要先统一精度。比如业务上能接受小数点后 5 位,那么就可以 round(2.2*3, 5) == round(3.3*2, 5)。 (最近一直在做财务方面的系统。。。
|
21
knowckx OP @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 的情况再比较大于小于,真是坑死了 |
23
joApioVVx4M4X6Rf 2022-02-06 13:08:49 +08:00
@knowckx 是的,技术上可以选择更高精度的运算类型。业务上简单的直接 round 也行
|
25
knowckx OP 我刚思考了下,得出一个看上去奇怪的结论:
a <= b 应该是 a < b + epsilon a < b 应该也是 a < b + epsilon |
26
GeruzoniAnsasu 2022-02-06 13:55:42 +08:00
|
27
ipwx 2022-02-06 15:06:44 +08:00
@knowckx 其实主要问题是你那个例子里面是要条件完全闭合。。。
a < b 肯定是 a < b 没错,+epsilon 放进你那个例子还是会出问题。 所以只不过是涉及到 == 判断,你需要额外特殊第一优先处理罢了。。。 |
28
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 |
29
akazure 2022-02-06 15:25:25 +08:00
Python 有 Fraction 标准库,可以用分子分母算
https://docs.python.org/zh-cn/3/library/fractions.html |
30
leimao 2022-02-06 15:48:39 +08:00
哈哈,多踩几次坑就记住了 。
|
31
wangnimabenma 2022-02-06 15:57:00 +08:00
电脑没法用有限的空间存储无限的数据
1. 用高精度库 (还是会有精度丢失只是小到无法察觉或者是说可有可无) 2. 用分数表示 |
32
Jooooooooo 2022-02-06 16:20:45 +08:00
因为计算机只有二进制, 十进制下看起来有限的小数, 二进制下表达不了, 十进制 1/3 也搞不定.
|
33
des 2022-02-06 16:26:28 +08:00 via iPhone
楼上基本都说清楚了,我再补充一点
精度损失基本都发生在“进制转换”的过程中 举个栗子:“1 小时 20 分钟”转换成“多少小时”的数字,应该写成多少? 1.33333333 是不是? 反向的话也是同理,计算机是二进制,钟表分和秒是 60 进制 另一个问题来了,为什么不写成分数形式? 两个原因,1 、很多时候不需要那么高精度; 2 、计算起来性能太差,也费存储空间 顺带再科普一个知识,浮点数做加减乘除是不会损失精度的,提前是使用得当 |
36
whusnoopy 2022-02-06 17:11:11 +08:00
@knowckx #21
歪个楼讨论下「科班」的问题,非针对楼主,请不用在意,只是有感而发 是不是正儿八经的相关专业就叫科班? 如果我不学无术啥也没学挂科挂到退学,这种大家应该都不会认可是科班 那如果我是相关专业且是做题家,考试分数都挺高,但动手实践各种坑,这种怎么说?不确定现在学校里都是怎样,反正我上学那会(也是 985 本硕计算机专业),班上总有几个纯考试分不错,但真正代码都不一定会写,这种算科班么? 如果我不是相关专业,但系统性学习和理解并能实践相关知识,这种虽然没有对应文凭,是不是也可以算科班? |
37
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 |
38
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 。 |
40
0attocs 2022-02-06 18:42:13 +08:00
@knowckx #19 需要区分<和<=时的一种做法是考虑用 a <= b + delta 代替 a < b ,但一般只有在为某个涉及浮点数比较的分析而定义语义时需要考虑吧。一般情况下更应该考虑的是问题本身是否是 well-conditioned 的以及算法+实现本身是否 stable 。
|
42
maojun 2022-02-06 18:51:06 +08:00 via iPhone
很好奇业界的最佳实践是怎么样的,蹲一个大佬。另外,如果用分数结构来表示浮点数的话,貌似就可以回避很多计算过程中的精度损失了吧🤨不过不知道为什么很少看到有这么做的。
|
43
0attocs 2022-02-06 19:10:46 +08:00
@des #22 而且你提的“精度损失基本都发生在“进制转换”的过程中”说法本身也很奇怪。且不说多少涉及实数 /浮点数的问题 /程序里有你所谓的“进制转换”,你这里的 error 完全就是 rounding error ,跟什么进制转换没必然关系,只要需要浮点数 /有限精度表示就有可能引入 error 。都是二进制就没事了吗,2^-53 的 64 位浮点数表示会带来精度损失吗?
|
47
0attocs 2022-02-06 19:54:45 +08:00 via iPhone
@ipwx 加一个常量 tolerance epsilon 肯定比比没有要好,但还是有点蒙住眼睛骗自己问题解决了的味道,无法被称为“正确”。要知道浮点数的分布不是均匀的,ulp 不是一个常量,而 machine epsilon 只是定义为 1 右边 ulp 的一半。
一个比只加常量 tolerance epsilon 更好一点点的方案是根据比较的两数来 scale 这个 tolerance epsilon 。此时 x < y 会被定义为 | x - y | / | x | <= epsilon or | x - y | / | y | <= epsilon 。 |
48
littlewing 2022-02-06 20:00:02 +08:00
@whusnoopy 哪里都有杠精
|
49
sutra 2022-02-06 20:40:04 +08:00
需要注意的是 golang 那个是 float64 ,别的另外几个语言是 32 bit 的。
|
50
disk 2022-02-06 21:00:05 +08:00
看需求吧,有些情况可以考虑符号计算
|
51
ipwx 2022-02-06 21:54:10 +08:00
@0attocs | x - y | / | x | <= epsilon or | x - y | / | y | <= epsilon 也并不那么合理。
比如我 delta = x - y 是某一步运算。算到很后面我需要比较 delta 是不是和 0 相等。 那很自然应该 abs(delta) < epsilon 而不是 abs(delta) / abs(delta) < epsilon |
52
qdcanyun 2022-02-06 23:45:09 +08:00
|
53
jinliming2 2022-02-06 23:45:42 +08:00
@knowckx #18: https://go.dev/blog/constants#numbers 所有纯数字表达式(包括四则运算的结果、复数等)都是常量,会在编译时给你替换好。
|
54
icyalala 2022-02-07 02:04:33 +08:00 1
Python 和其他语言是正确的,编译时和运行时表现相同。
Go 才是特例:Go 会在编译时把 constant expressions 在编译时求值,用的还是高精度计算: https://go.dev/ref/spec#Constants 这样就会造成编译时和运行时表现不同。 这个问题 10 年前就有人提到了: https://github.com/golang/go/issues/2789 Go 的作者之一回复 "It is too late to change the language spec at this point." |
55
Hanggi 2022-02-07 03:19:47 +08:00
计算机计算整数是精确的,所以如果想要达到精确计算通常可以使用类似 Decimal 的库,或者用 BigInt 转换使用。
浮点数不管哪个语言都有这个问题,打开 chrome 控制台,在里面输入 0.1 + 0.2 回车试试。 |
56
whusnoopy 2022-02-07 08:55:53 +08:00
@littlewing #48
|
57
whusnoopy 2022-02-07 09:04:40 +08:00
@littlewing #48
不好意思前面按错快捷键直接发出去了 如果你觉得我是杠精,那不应该浪费时间再回我一句 如果是我的表述让其他人不适,请告知我不妥的地方,这样我才能意识到问题并去改正 如果是我想讨论的大家意识偏差有问题,那么我们回到问题本身,看是我的逻辑有问题还是定义有问题 在 #15 @ipwx 的回复里提了一句「科班才是硬道理」并有点赞,不管赞的是这句话,还是后面更详尽的解释,联想本站很多讨论和社会舆论,是有很多对科班有要求的地方 在 #21 @knowckx 的回复里,通过自己的专业和学习过程,说明自己是正儿八经的科班,并不认为科班就能避开主楼里提出和衍生出来的问题 那我在 #36 里想讨论的,如果大家有假定「科班可以避开相关问题」,楼主也列举了他的科班经历并表示同经历的很多人也还是会有疑惑,那到底是大家对「科班」的定义不一致,还是「科班可以避开相关问题」这个假设就不应该成立? |
58
joApioVVx4M4X6Rf 2022-02-07 10:00:23 +08:00
@qdcanyun 这个帖子总结的真棒!!学到了
|
59
pcell 2022-02-07 19:01:42 +08:00
其实我更好奇 excel 是怎么处理小数点,财务都用 excel 记数应该是没有这种问题。
|
60
jinliming2 2022-02-10 01:47:32 +08:00
@pcell excel 也存在问题,但是现在比较新的版本都做了近似补偿: https://docs.microsoft.com/en-us/office/troubleshoot/excel/floating-point-arithmetic-inaccurate-result
比如输入公式:=1.333+1.225-1.333-1.225 可以得到 0 ,但是输入 =(1.333+1.225-1.333-1.225) 就得到了 -2.22045E-16 |