请教一个 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

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

4781 次点击
所在节点    Python
60 条回复
gstqc
2022-02-06 01:44:56 +08:00
chevalier
2022-02-06 01:46:54 +08:00
跟 Python 没关系,了解一下浮点数的原理,所有的语言都这样

正确的比较,使用语言自带的浮点库,或者 v1-v2 < 0.0000……01 这样
knowckx
2022-02-06 01:47:46 +08:00
@gstqc
谢谢引用,所以
v1>=v2 要改写成
math.isclose(v1, v2) or v1 > v2

这样有点繁琐了吧……
knowckx
2022-02-06 01:51:19 +08:00
@chevalier
啊,我理解部分小数无法精确存储
只是其他语言没这么麻烦的
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
gstqc
2022-02-06 02:04:19 +08:00
@knowckx 哪个语言的浮点数可以直接 ==?
yaojin
2022-02-06 02:10:39 +08:00
该睡了,还是说你是海外的,太卷了吧
lunaticus7
2022-02-06 02:14:52 +08:00
https://docs.python.org/3/library/decimal.html
想要精确小数的话可以用 decimal
knowckx
2022-02-06 02:18:24 +08:00
@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
knowckx
2022-02-06 02:19:44 +08:00
@secondwtq
我回头看下 谢谢
knowckx
2022-02-06 02:19:59 +08:00
@yaojin 习惯了熬夜……
knowckx
2022-02-06 02:21:14 +08:00
@lunaticus7 这个方式不错,就是麻烦点,要把所有可能要比较的浮点数都用 decimal 转一遍
iBugOne
2022-02-06 02:48:35 +08:00
@knowckx Go 语言里 2.2*3 这种写法不涉及浮点运算,因为它是一个常量。大部分 C 语言编译器也会做这样的优化,而 Python 是“写啥跑啥”的,所以只有 Python 是真的创建了两个浮点数和两个整数并且做浮点乘法的。

Go 换成这种写法你就发现区别了:

c1 := 2.2
c2 := 3.3
v1 := c1 * 3
v2 := c2 * 2
LeeReamond
2022-02-06 09:03:54 +08:00
没有什么简单的办法,首先是类 C 语言的通用问题,其次是 py 里想简写的话,运算符可以重载,但只能发生在对象上,所以还要全局钩子设置对象,很繁琐,不如接受全球程序员都接受的事实。
ipwx
2022-02-06 10:04:48 +08:00
唉,又疯了一个。。。不得不感慨科班还是硬道理。
----

“我理解部分小数无法精确存储” —— 所有不能写成 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 会输出等于我是震惊的。
ipwx
2022-02-06 10:06:28 +08:00
哦对 epsilon 是一个自己可以控制的小数常量,根据业务需求定。比如一般我会取 epsilon = 1e-7 (对 float 也一般有效了)。但是对 double 而言你也许可以使用 epsilon = 1e-12 。但无论如何自己能控制比语言帮你处理(却不知道到底怎么做的)要安全多了。
rcocco
2022-02-06 10:33:51 +08:00
坑在于你输入的值(屏幕上显示的值)和实际值并不一样,当你输入 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 后的结果可能比原来大,也可能小,还可能等。
所以浮点数比较只能作差取绝对值,差小于某个很小的数就认为是相等。
knowckx
2022-02-06 11:30:23 +08:00
@iBugOne 感谢回复,我跑了下确实有区别,但是你提到的
2.2*3 是一个常量 我没有理解,google 了下也没搜到什么内容,似乎是 go 对常量表达式有优化
agagega
2022-02-06 12:44:46 +08:00
void foo(double);

int main(void) {
foo(2.2 * 3);
foo(3.3 * 2);
return 0;
}

// clang -Ofast -S -emit-llvm -o -

define i32 @main() local_unnamed_addr #0 {
tail call void @foo(double 0x401A666666666667) #2
tail call void @foo(double 6.600000e+00) #2
ret i32 0
}

这俩是不一样的
0x0208v0
2022-02-06 13:02:53 +08:00
从业务角度来说的话,比较的时候,需要先统一精度。比如业务上能接受小数点后 5 位,那么就可以 round(2.2*3, 5) == round(3.3*2, 5)。 (最近一直在做财务方面的系统。。。

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

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

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

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

© 2021 V2EX